Java8实战-使用Stream

简介: Java8实战-使用Stream

使用流

在上一篇的读书笔记中,我们已经看到了流让你从外部迭代转向内部迭代。这样,你就用不着写下面这样的代码来显式地管理数据集合的迭代(外部迭代)了:

/**
 * 菜单
 */
public static final List<Dish> MENU =
        Arrays.asList(new Dish("pork", false, 800, Dish.Type.MEAT),
                new Dish("beef", false, 700, Dish.Type.MEAT),
                new Dish("chicken", false, 400, Dish.Type.MEAT),
                new Dish("french fries", true, 530, Dish.Type.OTHER),
                new Dish("rice", true, 350, Dish.Type.OTHER),
                new Dish("season fruit", true, 120, Dish.Type.OTHER),
                new Dish("pizza", true, 550, Dish.Type.OTHER),
                new Dish("prawns", false, 400, Dish.Type.FISH),
                new Dish("salmon", false, 450, Dish.Type.FISH));
复制代码
List<Dish> menu = Dish.MENU;
List<Dish> vegetarianDishes = new ArrayList<>();
for(Dish d: menu){
    if(d.isVegetarian()){
        vegetarianDishes.add(d);
    }
}
复制代码

我们可以使用支持 filter 和 collect 操作的Stream API(内部迭代)管理对集合数据的迭代。 你只需要将筛选行为作为参数传递给 filter 方法就行了。

List<Dish> vegetarianDishes =
                menu.stream()
                        .filter(Dish::isVegetarian)
                        .collect(toList());
复制代码

这种处理数据的方式很有用,因为你让StreamAPI管理如何处理数据。这样StreamAPI就可以在背后进行多种优化。此外,使用内部迭代的话,StreamAPI可以决定并行运行你的代码。这要是用外部迭代的话就办不到了,因为你只能用单一线程挨个迭代。接下来,你将会看到StreamAPI支持的许多操作。这些操作能让你快速完成复杂的数据查询,如筛选、切片、映射、查找、匹配和归约。

切片和筛选

我们来看看如何选择流中的元素:用谓词筛选,筛选出各不相同的元素,忽略流中的头几个元素,或将流截短至指定长度。

用谓词筛选

Streams 接口支持 filter方法(你现在应该很熟悉了)。该操作会接受一个谓词(一个返回boolean 的函数)作为参数,并返回一个包括所有符合谓词的元素的流。

List<Dish> vegetarianDishes =
                menu.stream()
                        // 方法引用检查菜肴是否适合素食者
                        .filter(Dish::isVegetarian)
                        .collect(toList());
复制代码

筛选各异的元素

流还支持一个叫作 distinct 的方法,它会返回一个元素各异(根据流所生成元素的hashCode 和 equals 方法实现)的流。例如,以下代码会筛选出列表中所有的偶数,并确保没有重复。

List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
        numbers.stream()
                .filter(i -> i % 2 == 0)
                .distinct()
                .forEach(System.out::println);
复制代码

首先是筛选出偶数,然后检查是否有重复,最后打印。

截短流

流支持 limit(n) 方法,该方法会返回一个不超过给定长度的流。所需的长度作为参数传递 给 limit 。如果流是有序的,则最多会返回前 n 个元素。比如,你可以建立一个 List ,选出热量超过300卡路里的头三道菜:

List<Dish> dishes = menu.stream()
                .filter(d -> d.getCalories() > 300)
                .limit(3)
                .collect(toList());
// pork beef chicken
dishes.forEach(dish -> System.out.println(dish.getName()));
复制代码

上面的代码展示了filter和limit的组合。我们可以看到,该方法之筛选出来了符合谓词的头三个元素,然后就立即返回了结果。请注意limit也可以放在无序流上比如源是一个 Set 。这种情况下, limit 的结果不会以任何顺序排列。

跳过元素

流还支持 skip(n) 方法,返回一个扔掉了前n个元素的流。如果流中元素不足n个,则返回一个空流。请注意,limit(n)和skip(n)是互补的!例如,下面的代码将跳过超过300卡路里的头两道菜,并返回剩下的。

List<Dish> dishes = menu.stream()
                .filter(d -> d.getCalories() > 300)
                // 跳过前两个
                .skip(2)
                .collect(toList());
// chicken french fries rice pizza prawns salmon
dishes.forEach(dish -> System.out.println(dish.getName()));
复制代码

映射

一个非常常见的数据处理套路就是从某些对象中选择信息。比如在SQL里,你可以从表中选择一列。Stream API也通过 map 和 flatMap 方法提供了类似的工具。

对流中每一个元素应用函数

流支持 map 方法,它会接受一个函数作为参数。这个函数会被应用到每个元素上,并将其映 射成一个新的元素(使用映射一词,是因为它和转换类似,但其中的细微差别在于它是“创建一 个新版本”而不是去“修改”)。例如,下面的代码把方法引用 Dish::getName 传给了 map 方法,来提取流中菜肴的名称:

List<String> dishNames = menu.stream()
                .map(Dish::getName)
                .collect(toList());
// [pork, beef, chicken, french fries, rice, season fruit, pizza, prawns, salmon]
System.out.println(dishNames);
复制代码

getName方法返回的是一个String,所以map方法输出的流类型就是Stream。当然,我们也可以获取通过map获取其他的属性。比如:我需要知道这个菜单的名字有多长,那么我们可以这样做:

List<Integer> len = menu.stream()
                .map(dish -> dish.getName().length())
                .collect(toList());
// [4, 4, 7, 12, 4, 12, 5, 6, 6]
System.out.println(len);
复制代码

是的,就是这么简单,当我们只需要获取某个对象中的某个属性时,通过map就可以实现了。

流的扁平化

你已经看到如何使用 map方法返回列表中每个菜单名称的长度了。让我们拓展一下:对于一张单词 表 , 如 何 返 回 一 张 列 表 , 列 出 里 面 各 不 相 同 的 字 符 呢 ? 例 如 , 给 定 单 词 列 表["Hello","World"] ,你想要返回列表 ["H","e","l", "o","W","r","d"] 。

你可能马上会想到,将每个单词映射成一张字符表,然后调用distance 来过滤重复的字符。

List<String> words = Arrays.asList("Hello", "World");
List<String[]> wordList = words.stream()
        .map(word -> word.split(""))
        .distinct()
        .collect(Collectors.toList());
wordList.forEach(wordArray -> {
    for (String s : wordArray) {
        System.out.print(s);
    }
    System.out.println();
});
复制代码

执行结果:

Hello
World
复制代码

执行完后一看,不对呀。仔细想一想:我们把["Hello", "World"]这两个单词把它们分割称为了字符数组,["H", "e", "l", "l", "o"],["W", "o", "r", "l", "d"]。然后将这个字符数组去判断是否重复,不是一个字符是否重复,而是这一个字符数组是否有重复。所以,打印出来就是Hello World。

幸好可以用flatMap来解决这个问题!让我们一步步地来解决它。

  1. 尝试使用 map 和 Arrays.stream()
首先,我们需要一个字符流,而不是数组流。有一个叫作Arrays.stream()的方法可以接受
一个数组并产生一个流,例如:
String[] arrayOfWords = {"Hello", "World"};
Stream<String> streamOfwords = Arrays.stream(arrayOfWords);
按照刚刚上面的做法,使用map和Arrays.stream(),显然是不行的。
这是因为,你现在得到的是一个流的列表(更准确地说是Stream<String>)!的确,
你先是把每个单词转换成一个字母数组,然后把每个数组变成了一个独立的流。
复制代码
  1. 使用 flatMap
我们可以像下面这样使用flatMap来解决这个问题:
String[] arrayOfWords = {"Hello", "World"};
Stream<String> streamOfwords = Arrays.stream(arrayOfWords);
List<String> uniqueCharacters = streamOfwords
        // 将每个单词转换为由其字母构成的数组
        .map(w -> w.split(""))
        // 将各个生成流扁平化为单个流
        .flatMap(Arrays::stream)
        .distinct()
        .collect(Collectors.toList());
// HeloWrd
uniqueCharacters.forEach(System.out::print);
复制代码

太棒了,实现了我们想要的效果!使用flatMap方法的效果是,各个数组并不是分别映射成为一个流,而是映射成流的内容。所有使用map(s -> split(""))时生成的单个流都被合并起来,即扁平化为一个流。一言以蔽之, flatMap 方法让你把一个流中的每个值都换成另一个流,然后把所有的流连接起来成为一个流。

查找和匹配

另一个常见的数据处理套路是看看数据集中的某些元素是否匹配一个给定的属性。Stream API通过 allMatch 、 anyMatch 、 noneMatch 、 findFirst 和 findAny 方法提供了这样的工具。

检查谓词是否至少匹配一个元素

anyMatch 方法可以回答“流中是否有一个元素能匹配给定的谓词”。比如,你可以用它来看 看菜单里面是否有素食可选择:

if(menu.stream().anyMatch(Dish::isVegetarian)){
    System.out.println("有素菜,不用担心!");
}
复制代码

anyMatch 方法返回一个 boolean ,因此是一个终端操作。

检查谓词是否匹配所有元素

allMatch 方法的工作原理和 anyMatch 类似,但它会看看流中的元素是否都能匹配给定的谓词。比如,你可以用它来看看菜品是否有利健康(即所有菜的热量都低于1000卡路里):

boolean isHealthy = menu.stream().allMatch(d -> d.getCalories() < 1000);
复制代码

** noneMatch ** 和 allMatch 相对的是 noneMatch 。它可以确保流中没有任何元素与给定的谓词匹配。比如, 你可以用 noneMatch 重写前面的例子:

boolean isHealthy = menu.stream().noneMatch(d -> d.getCalories() >= 1000);
复制代码

anyMatch 、 allMatch 和 noneMatch 这三个操作都用到了我们所谓的短路,这就是大家熟悉 的Java中 && 和 || 运算符短路在流中的版本。

查找元素

findAny方法返回当前流中的任意元素。它可以与其他流结合操作使用。比如,你可能想找到一道素食菜肴。我们可以使用filter和findAny来实现:

Optional<Dish> dish = menu.stream()
                .filter(Dish::isVegetarian)
                .findAny();
复制代码

OK,这样就完成我们想要的了。但是,你会发现它返回的是一个Optional。Optional类(java.util.Optional)是一个容器类,代表一个值存在或者不存在。在上面的代码中,findAny可能什么都没找到。。Java 8的库设计人员引入了 Optional ,这 样就不用返回众所周知容易出问题的 null 了。很好的解决了“十亿美元的错误”!不过我们现在不讨论它,以后再去详细的了解它是如何的使用。

查找第一个元素

有些流有一个出现顺序(encounter order)来指定流中项目出现的逻辑顺序(比如由 List 或 排序好的数据列生成的流)。对于这种流,你可能想要找到第一个元素。为此有一个 findFirst 方法,它的工作方式类似于 findany 。例如,给定一个数字列表,下面的代码能找出第一个平方 能被3整除的数:

List<Integer> someNumbers = Arrays.asList(1, 2, 3, 4, 5, 6);
Optional<Integer> firstSquareDivisibleByThree =
        someNumbers.stream()
                .map(x -> x * x)
                .filter(x -> x % 3 == 0)
                // 9
                .findFirst();
复制代码

是的,通过链式调用,就完成了我们想要的功能,比起以前来说好太多了。你可能有一个疑问,findAny和findFrist在什么时候使用比较好或者说两个都存在怎么办。findAny和findFrist是并行的。找到第一个元素在并行上限制的更多。如果,你不关心放回元素是哪一个,请使用findAny,因为它在使用并行流时限制比较少。

归约

到目前为止,我们见到过的终端操作都是返回一个 boolean ( allMatch 之类的)、 void ( forEach )或 Optional 对象( findAny 等)。你也见过了使用 collect 来将流中的所有元素组合成一个 List 。接下来,我们将会看到如何把一个流中的元素组合起来,使用reduce操作来表达更复杂的查询,比如“计算菜单中的总卡路里”或者“菜单中卡路里最高的菜是哪一个”。此类查询需要将流中的所有元素反复结合起来,得到一个值,比如一个Integer。这样的查询可以被归类为归约操作(将流归约成一个值)。用函数式编程语言的术语来说,这称为折叠(fold),因为你可以将这个操作看成把一张长长的纸(你的流)反复折叠成一个小方块,而这就是折叠操作的结果。

元素求和

在没有reduce之前,我们先用foreach循环来对数字列表中的元素求和:

int sum = 0;
for (int x : numbers) {
    sum += x;
}
复制代码

numbers 中的每个元素都用加法运算符反复迭代来得到结果。通过反复使用加法,你把一个 数字列表归约成了一个数字。

要是还能把所有的数字相乘,而不必去复制粘贴这段代码,岂不是很好?这正是 reduce 操 作的用武之地,它对这种重复应用的模式做了抽象。你可以像下面这样对流中所有的元素求和:

List<Integer> numbers = Arrays.asList(3, 4, 5, 1, 2);
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
// 15
System.out.println(sum);
复制代码

我们很简单的就完成了元素与元素相加最后得到的结果。如果是元素与元素相乘,也很简单:

numbers.stream().reduce(1, (a, b) -> a * b);
复制代码

是的,就是这么简单!我们还可以使用方法引用来简化求和的代码,让它看起来更加简洁:

int sum2 = numbers.stream().reduce(0, Integer::sum);
复制代码

** 无初始值 ** reduce 还有一个重载的变体,它不接受初始值,但是会返回一个 Optional 对象:

Optional<Integer> sum = numbers.stream().reduce((a, b) -> (a + b));
复制代码

为什么它返回一个 Optional 呢?考虑流中没有任何元素的情况。 reduce 操作无 法返回其和,因为它没有初始值。这就是为什么结果被包裹在一个 Optional 对象里,以表明和 可能不存在。现在看看用 reduce 还能做什么。

最大值和最小值

原来,只要用归约就可以计算最大值和最小值了!让我们来看看如何利用刚刚学到的 reduce 来计算流中最大或最小的元素。

Optional<Integer> max = numbers.stream().reduce(Integer::max);
复制代码

reduce 操作会考虑新值和流中下一个元素,并产生一个新的最大值,直到整个流消耗完!就像这样:

3 - 4 - 5 - 1 - 2
3 → 4
    4 → 5
        5 → 1
            5 → 2
                5
复制代码

通过这样的形式去比较哪个数值是最大的!如果,你获取最小的数值,也很简单只需要这样:

Optional<Integer> min = numbers.stream().reduce(Integer::min);

付诸实战

在本节中,我们会将迄今学到的关于流的知识付诸实践。我们来看一个不同的领域:执行交易的交易员。你的经理让你为八个查询找到答案。

  1. 找出2011年发生的所有交易,并按交易额排序(从低到高)。
  2. 交易员都在哪些不同的城市工作过?
  3. 查找所有来自于剑桥的交易员,并按姓名排序。
  4. 返回所有交易员的姓名字符串,按字母顺序排序。
  5. 有没有交易员是在米兰工作的?
  6. 打印生活在剑桥的交易员的所有交易额。
  7. 所有交易中,最高的交易额是多少?
  8. 找到交易额最小的交易。

领域:交易员和交易

以下是我们要处理的领域,一个 Traders 和 Transactions 的列表:

Trader raoul = new Trader("Raoul", "Cambridge");
Trader mario = new Trader("Mario", "Milan");
Trader alan = new Trader("Alan", "Cambridge");
Trader brian = new Trader("Brian", "Cambridge");

List<Transaction> transactions = Arrays.asList(
        new Transaction(brian, 2011, 300),
        new Transaction(raoul, 2012, 1000),
        new Transaction(raoul, 2011, 400),
        new Transaction(mario, 2012, 710),
        new Transaction(mario, 2012, 700),
        new Transaction(alan, 2012, 950)
);
复制代码

Trader和Transaction类的定义:

public class Trader {
    private String name;
    private String city;

    public Trader(String n, String c){
        this.name = n;
        this.city = c;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getCity() {
        return city;
    }

    public void setCity(String city) {
        this.city = city;
    }

    @Override
    public String toString() {
        return "Trader{" +
                "name='" + name + '\'' +
                ", city='" + city + '\'' +
                '}';
    }
}
复制代码

Transaction类:

public class Transaction {
    private Trader trader;
    private Integer year;
    private Integer value;

    public Transaction(Trader trader, Integer year, Integer value) {
        this.trader = trader;
        this.year = year;
        this.value = value;
    }

    public Trader getTrader() {
        return trader;
    }

    public void setTrader(Trader trader) {
        this.trader = trader;
    }

    public Integer getYear() {
        return year;
    }

    public void setYear(Integer year) {
        this.year = year;
    }

    public Integer getValue() {
        return value;
    }

    public void setValue(Integer value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return "Transaction{" +
                "trader=" + trader +
                ", year=" + year +
                ", value=" + value +
                '}';
    }
}
复制代码
首先,我们来看第一个问题:找出2011年发生的所有交易,并按交易额排序(从低到高)。

复制代码

List<Transaction> tr2011 = transactions.stream()
                // 筛选出2011年发生的所有交易
                .filter(transaction -> transaction.getYear() == 2011)
                // 按照交易额从低到高排序
                .sorted(Comparator.comparing(Transaction::getValue))
                // 转为集合
                .collect(Collectors.toList());
复制代码

太棒了,第一个问题我们很轻松的就解决了!首先,将transactions集合转为流,然后给filter传递一个谓词来选择2011年的交易,接着按照交易额从低到高进行排序,最后将Stream中的所有元素收集到一个List集合中。

第二个问题:交易员都在哪些不同的城市工作过?
List<Transaction> tr2011 = transactions.stream()
                // 筛选出2011年发生的所有交易
                .filter(transaction -> transaction.getYear() == 2011)
                // 按照交易额从低到高排序
                .sorted(Comparator.comparing(Transaction::getValue))
                // 转为集合
                .collect(Collectors.toList());
复制代码

是的,我们很简单的完成了第二个问题。首先,将transactions集合转为流,然后使用map提取出与交易员相关的每位交易员所在的城市,接着使用distinct去除重复的城市(当然,我们也可以去掉distinct,在最后我们就要使用collect,将Stream中的元素转为一个Set集合。collect(Collectors.toSet())),我们只需要不同的城市,最后将Stream中的所有元素收集到一个List中。

第三个问题:查找所有来自于剑桥的交易员,并按姓名排序。
List<Trader> traders = transactions.stream()
                // 从交易中提取所有的交易员
                .map(Transaction::getTrader)
                // 进选择位于剑桥的交易员
                .filter(trader -> "Cambridge".equals(trader.getCity()))
                // 确保没有重复
                .distinct()
                // 对生成的交易员流按照姓名进行排序
                .sorted(Comparator.comparing(Trader::getName))
                .collect(Collectors.toList());
复制代码

第三个问题,从交易中提取所有的交易员,然后进选择位于剑桥的交易员确保没有重复,接着对生成的交易员流按照姓名进行排序。

第四个问题:返回所有交易员的姓名字符串,按字母顺序排序。

复制代码

String traderStr =
                transactions.stream()
                        // 提取所有交易员姓名,生成一个 Strings 构成的 Stream
                        .map(transaction -> transaction.getTrader().getName())
                        // 只选择不相同的姓名
                        .distinct()
                        // 对姓名按字母顺序排序
                        .sorted()
                        // 逐个拼接每个名字,得到一个将所有名字连接起来的 String
                        .reduce("", (n1, n2) -> n1 + " " + n2);
复制代码

这些问题,我们都很轻松的就完成!首先,提取所有交易员姓名,生成一个 Strings 构成的 Stream并且只选择不相同的姓名,然后对姓名按字母顺序排序,最后使用reduce将名字拼接起来!

请注意,此解决方案效率不高(所有字符串都被反复连接,每次迭代的时候都要建立一个新 的 String 对象)。下一章中,你将看到一个更为高效的解决方案,它像下面这样使用 joining (其 内部会用到 StringBuilder ):

String traderStr =
                transactions.stream()
                            .map(transaction -> transaction.getTrader().getName())
                            .distinct()
                            .sorted()
                            .collect(joining());
复制代码
第五个问题:有没有交易员是在米兰工作的?
boolean milanBased =
                transactions.stream()
                        // 把一个谓词传递给 anyMatch ,检查是否有交易员在米兰工作
                        .anyMatch(transaction -> "Milan".equals(transaction.getTrader()
                                .getCity()));
复制代码

第五个问题,依旧很简单把一个谓词传递给 anyMatch ,检查是否有交易员在米兰工作。

第六个问题:打印生活在剑桥的交易员的所有交易额。
transactions.stream()
                // 选择住在剑桥的交易员所进行的交易
                .filter(t -> "Cambridge".equals(t.getTrader().getCity()))
                // 提取这些交易的交易额
                .map(Transaction::getValue)
                // 打印每个值
                .forEach(System.out::println);
复制代码

第六个问题,首先选择住在剑桥的交易员所进行的交易,接着提取这些交易的交易额,然后就打印出每个值。

第七个问题:所有交易中,最高的交易额是多少?
Optional<Integer> highestValue =
                transactions.stream()
                        // 提取每项交易的交易额
                        .map(Transaction::getValue)
                        // 计算生成的流中的最大值
                        .reduce(Integer::max);
复制代码

第七个问题,首先提取每项交易的交易额,然后使用reduce计算生成的流中的最大值。

第八个问题:找到交易额最小的交易。
Optional<Transaction> smallestTransaction = transactions.stream()
                                         .min(comparing(Transaction::getValue));
复制代码

是的,第八个问题很简单,但是还有更好的做法!流支持 min 和 max 方法,它们可以接受一个 Comparator 作为参数,指定 计算最小或最大值时要比较哪个键值:

Optional<Transaction> smallestTransaction = transactions.stream()
                                         .min(comparing(Transaction::getValue));
复制代码

上面的八个问题,我们通过Stream很轻松的就完成了,真是太棒了!

数值流

我们在前面看到了可以使用 reduce 方法计算流中元素的总和。例如,你可以像下面这样计 算菜单的热量:

int calories = menu.stream()
                    .map(Dish::getCalories)
                    .reduce(0, Integer::sum);
复制代码

这段代码的问题是,它有一个暗含的装箱成本。每个 Integer 都必须拆箱成一个原始类型, 再进行求和。要是可以直接像下面这样调用 sum 方法,岂不是更好?

int calories = menu.stream()
                    .map(Dish::getCalories)
                    .sum();
复制代码

但这是不可能的。问题在于 map 方法会生成一个 Stream 。虽然流中的元素是 Integer 类 型,但 Streams 接口没有定义 sum 方法。为什么没有呢?比方说,你只有一个像 menu 那样的Stream ,把各种菜加起来是没有任何意义的。但不要担心,Stream API还提供了原始类型流特化,专门支持处理数值流的方法。

原始类型流特化

Java 8引入了三个原始类型特化流接口来解决这个问题: IntStream 、 DoubleStream 和 LongStream ,分别将流中的元素特化为 int 、 long 和 double ,从而避免了暗含的装箱成本。每个接口都带来了进行常用数值归约的新方法,比如对数值流求和的 sum ,找到最大元素的max。此外还有在必要时再把它们转换回对象流的方法。要记住的是,这些特化的原因并不在于流的复杂性,而是装箱造成的复杂性——即类似 int 和 Integer 之间的效率差异。

1.映射到数值流

将流转换为特化版本的常用方法是 mapToInt 、 mapToDouble 和 mapToLong 。这些方法和前 面说的 map 方法的工作方式一样,只是它们返回的是一个特化流,而不是 Stream 。例如,我们可以像下面这样用 mapToInt 对 menu 中的卡路里求和:

int calories = menu.stream()
        // 返回一个IntStream
        .mapToInt(Dish::getCalories)
        .sum();
复制代码

这里, mapToInt 会从每道菜中提取热量(用一个 Integer 表示),并返回一个 IntStream (而不是一个 Stream )。然后你就可以调用 IntStream 接口中定义的 sum 方法,对卡 路里求和了!请注意,如果流是空的, sum 默认返回 0 。 IntStream 还支持其他的方便方法,如 max 、 min 、 average 等。

2.转换回对象流

同样,一旦有了数值流,你可能会想把它转换回非特化流。例如, IntStream 上的操作只能 产生原始整数: IntStream 的 map 操作接受的Lambda必须接受 int 并返回 int (一个 IntUnaryOperator )。但是你可能想要生成另一类值,比如 Dish 。为此,你需要访问 Stream 接口中定义的那些更广义的操作。要把原始流转换成一般流(每个 int 都会装箱成一个 Integer ),可以使用 boxed 方法,如下所示:

IntStream intStream = menu.stream().mapToInt(Dish::getCalories);
Stream<Integer> stream = intStream.boxed();
复制代码

3.默认值 OptionalInt

求和的那个例子很容易,因为它有一个默认值: 0 。但是,如果你要计算 IntStream 中的最 大元素,就得换个法子了,因为 0 是错误的结果。如何区分没有元素的流和最大值真的是 0 的流呢? 前面我们介绍了 Optional 类,这是一个可以表示值存在或不存在的容器。 Optional 可以用 Integer 、 String 等参考类型来参数化。对于三种原始流特化,也分别有一个 Optional 原始类 型特化版本: OptionalInt 、 OptionalDouble 和 OptionalLong 。

例如,要找到 IntStream 中的最大元素,可以调用 max 方法,它会返回一个 OptionalInt :

OptionalInt maxCalories = menu.stream()
                .mapToInt(Dish::getCalories)
                .max();
复制代码

现在,如果没有最大值的话,你就可以显式处理 OptionalInt 去定义一个默认值了:

int max = maxCalories.orElse(1);
复制代码

数值范围

和数字打交道时,有一个常用的东西就是数值范围。比如,假设你想要生成1和100之间的所有数字。Java 8引入了两个可以用于 IntStream 和 LongStream 的静态方法,帮助生成这种范围: range 和 rangeClosed 。这两个方法都是第一个参数接受起始值,第二个参数接受结束值。但 range 是不包含结束值的,而 rangeClosed 则包含结束值。让我们来看一个例子:

// 一个从1到100的偶数流 包含结束值
IntStream evenNumbers = IntStream.rangeClosed(1, 100)
        .filter(n -> n % 2 == 0);
// 从1到100共有50个偶数
System.out.println(evenNumbers.count());
复制代码

这里我们用了 rangeClosed 方法来生成1到100之间的所有数字。它会产生一个流,然后你 可以链接 filter 方法,只选出偶数。到目前为止还没有进行任何计算。最后,你对生成的流调 用 count 。因为 count 是一个终端操作,所以它会处理流,并返回结果 50 ,这正是1到100(包括 两端)中所有偶数的个数。请注意,比较一下,如果改用 IntStream.range(1, 100) ,则结果 将会是 49 个偶数,因为 range 是不包含结束值的。

构建流

希望到现在,我们已经让你相信,流对于表达数据处理查询是非常强大而有用的。到目前为 止,你已经能够使用 stream 方法从集合生成流了。此外,我们还介绍了如何根据数值范围创建 数值流。但创建流的方法还有许多!本节将介绍如何从值序列、数组、文件来创建流,甚至由生成函数来创建无限流!

由值创建流

你可以使用静态方法 Stream.of ,通过显式值创建一个流。它可以接受任意数量的参数。例 如,以下代码直接使用 Stream.of 创建了一个字符串流。然后,你可以将字符串转换为大写,再 一个个打印出来:

Stream<String> stream = Stream.of("Java 8 ", "Lambdas ", "In ", "Action");
stream.map(String::toUpperCase).forEach(System.out::println);
复制代码

你可以使用 empty 得到一个空流,如下所示:

Stream<String> emptyStream = Stream.empty();
复制代码

由数组创建流

我们可以使用静态方法 Arrays.stream 从数组创建一个流。它接受一个数组作为参数。例如, 我们可以将一个原始类型 int 的数组转换成一个 IntStream ,如下所示:

int[] numbers = {2, 3, 5, 7, 11, 13};
// 总和41
int sum = Arrays.stream(numbers).sum();
复制代码
由文件生成流

Java中用于处理文件等I/O操作的NIO API(非阻塞 I/O)已更新,以便利用Stream API。 java.nio.file.Files 中的很多静态方法都会返回一个流。例如,一个很有用的方法是 Files.lines ,它会返回一个由指定文件中的各行构成的字符串流。使用我们迄今所学的内容,我们可以用这个方法看看一个文件中有多少各不相同的词:

long uniqueWords;
try (Stream<String> lines = Files.lines(Paths.get(ClassLoader.getSystemResource("data.txt").toURI()),
        Charset.defaultCharset())) {
    uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" ")))
            .distinct()
            .count();
    System.out.println("uniqueWords:" + uniqueWords);
} catch (IOException e) {
    e.fillInStackTrace();
} catch (URISyntaxException e) {
    e.printStackTrace();
}
复制代码

你可以使用 Files.lines 得到一个流,其中的每个元素都是给定文件中的一行。然后,你 可以对 line 调用 split 方法将行拆分成单词。应该注意的是,你该如何使用 flatMap 产生一个扁平的单词流,而不是给每一行生成一个单词流。最后,把 distinct 和 count 方法链接起来,数数流中有多少各不相同的单词。

由函数生成流:创建无限流

Stream API提供了两个静态方法来从函数生成流: Stream.iterate 和 Stream.generate 。 这两个操作可以创建所谓的无限流:不像从固定集合创建的流那样有固定大小的流。由 iterate和 generate 产生的流会用给定的函数按需创建值,因此可以无穷无尽地计算下去!一般来说,应该使用 limit(n) 来对这种流加以限制,以避免打印无穷多个值。

1.迭代

我们先来看一个 iterate 的简单例子,然后再解释:

Stream.iterate(0, n -> n + 2)
        .limit(10)
        .forEach(System.out::println);
复制代码

iterate 方法接受一个初始值(在这里是 0 ),还有一个依次应用在每个产生的新值上的 Lambda( UnaryOperator 类型)。这里,我们使用Lambda  n -> n + 2 ,返回的是前一个元素加上2。因此,iterate方法生成了一个所有正偶数的流:流的第一个元素是初始值 0 。然后加上 2 来生成新的值 2 ,再加上 2 来得到新的值 4 ,以此类推。这种 iterate 操作基本上是顺序的,因为结果取决于前一次应用。请注意,此操作将生成一个无限流——这个流没有结尾,因为值是按需计算的,可以永远计算下去。我们说这个流是无界的。正如我们前面所讨论的,这是流和集合之间的一个关键区别。我们使用limit方法来显式限制流的大小。这里只选择了前10个偶数。然后可以调用 forEach 终端操作来消费流,并分别打印每个元素。

2.生成

与 iterate 方法类似, generate 方法也可让你按需生成一个无限流。但 generate 不是依次 对每个新生成的值应用函数的。它接受一个 Supplier 类型的Lambda提供新的值。我们先来 看一个简单的用法:

Stream.generate(Math::random)
                .limit(5)
                .forEach(System.out::println);
复制代码

这段代码将生成一个流,其中有五个0到1之间的随机双精度数。例如,运行一次得到了下面 的结果:

0.8404010101858976
0.03607897810804739
0.025199243727344833
0.8368092999566692
0.14685668895309267
复制代码

Math.Random 静态方法被用作新值生成器。同样,你可以用 limit 方法显式限制流的大小, 否则流将会无限长。

你可能想知道, generate 方法还有什么用途。我们使用的供应源(指向 Math.random 的方 法引用)是无状态的:它不会在任何地方记录任何值,以备以后计算使用。但供应源不一定是无状态的。你可以创建存储状态的供应源,它可以修改状态,并在为流生成下一个值时使用。

我们在这个例子中会使用 IntStream 说明避免装箱操作的代码。 IntStream 的 generate 方 法会接受一个 IntSupplier ,而不是 Supplier 。例如,可以这样来生成一个全是1的无限流:

IntStream ones = IntStream.generate(() -> 1);
复制代码

还记得第三章的笔记中,Lambda允许你创建函数式接口的实例,只要直接内联提供方法的实 现就可以。你也可以像下面这样,通过实现 IntSupplier 接口中定义的 getAsInt 方法显式传递一个对象(虽然这看起来是无缘无故地绕圈子,也请你耐心看):

IntStream twos = IntStream.generate(new IntSupplier(){
            @Override
            public int getAsInt(){
                return 2;
            }
        });
复制代码

generate 方法将使用给定的供应源,并反复调用 getAsInt 方法,而这个方法总是返回 2 。 但这里使用的匿名类和Lambda的区别在于,匿名类可以通过字段定义状态,而状态又可以用 getAsInt 方法来修改。这是一个副作用的例子。我们迄今见过的所有Lambda都是没有副作用的;它们没有改变任何状态。

总结

这一章的东西很多,收获也很多!现在你可以更高效地处理集合了。事实上,流让你可以简洁地表达复杂的数据处理查询。此外,流可以透明地并行化。以下是我们应从本章中学到的关键概念。 这一章的读书笔记中,我们学习和了解到了:

  1. Streams API可以表达复杂的数据处理查询。
  2. 你可以使用 filter 、 distinct 、 skip 和 limit 对流做筛选和切片。
  3. 你可以使用 map 和 flatMap 提取或转换流中的元素。
  4. 你可以使用 findFirst 和 findAny 方法查找流中的元素。你可以用 allMatch、noneMatch 和 anyMatch 方法让流匹配给定的谓词。
  5. 这些方法都利用了短路:找到结果就立即停止计算;没有必要处理整个流。
  6. 你可以利用 reduce 方法将流中所有的元素迭代合并成一个结果,例如求和或查找最大 元素。
  7. filter 和 map 等操作是无状态的,它们并不存储任何状态。 reduce 等操作要存储状态才 能计算出一个值。 sorted 和 distinct 等操作也要存储状态,因为它们需要把流中的所 有元素缓存起来才能返回一个新的流。这种操作称为有状态操作。
  8. 流有三种基本的原始类型特化: IntStream 、 DoubleStream 和 LongStream 。它们的操 作也有相应的特化。
  9. 流不仅可以从集合创建,也可从值、数组、文件以及 iterate 与 generate 等特定方法 创建。
  10. 无限流是没有固定大小的流。


目录
相关文章
|
4天前
|
Java API
深入探讨 Java 8 集合操作:全面解析 Stream API 的强大功能
深入探讨 Java 8 集合操作:全面解析 Stream API 的强大功能
14 2
|
4天前
|
缓存 NoSQL Java
Java高并发实战:利用线程池和Redis实现高效数据入库
Java高并发实战:利用线程池和Redis实现高效数据入库
20 0
|
5天前
|
安全 Java 程序员
Java8实战-新的日期和时间API
Java8实战-新的日期和时间API
15 3
|
1天前
|
Java 索引
Java List实战:手把手教你玩转ArrayList和LinkedList
【6月更文挑战第17天】在Java中,ArrayList和LinkedList是List接口的实现,分别基于动态数组和双向链表。ArrayList适合索引访问,提供快速读取,而LinkedList擅长插入和删除操作。通过示例展示了两者的基本用法,如添加、访问、修改和删除元素。根据场景选择合适的实现能优化性能。
|
3天前
|
Java 开发者
Java 面向对象编程实战:从类定义到对象应用,让你成为高手!
【6月更文挑战第15天】在Java中,掌握面向对象编程至关重要。通过创建`Book`类,展示了属性如`title`和`author`,以及构造方法和getter方法。实例化对象如`book1`和`book2`,并访问其属性。进一步扩展类,添加`pages`和`calculateReadingTime`方法,显示了类的可扩展性。在更大规模的项目中,如电商系统,可以定义`Product`、`User`和`Order`类,利用对象表示实体和它们的交互。实践是精通Java OOP的关键,不断学习和应用以提升技能。
|
3天前
|
设计模式 Java
一文掌握 Java 面向对象精髓:从类定义到对象实战
【6月更文挑战第15天】Java面向对象编程初学者指南:类是对象模板,如`Person`类含`name`和`age`属性。创建对象用`new`,如`Person person = new Person()`。访问属性如`person.name=&quot;Alice&quot;`,调用方法如`person.sayHello()`。类能继承,如`Student extends Person`。对象间共享数据可传参或共用引用。多态性允许父类引用调用子类方法。注意对象生命周期和内存管理,避免内存泄漏。通过实践和理解这些基础,提升编程技能。
|
4天前
|
Java 编译器 程序员
【实战攻略】Java高手教你如何灵活运用if-else和switch,提升代码效率!
【6月更文挑战第14天】本文探讨了Java中if-else和switch语句的巧妙运用,通过示例展示了如何提升代码效率和可读性。通过使用Map重构if-else结构,使代码更简洁易维护;利用switch处理枚举类型,实现清晰的代码结构。在性能方面,switch在选项少时占优,而现代JIT编译器优化后的if-else适用于大规模字符串比较。理解并灵活运用这两种控制结构,能助你在Java编程中写出高效、易读的代码。
|
4天前
|
监控 Java Spring
Java 动态代理详解与实战示例
Java 动态代理详解与实战示例
4 1
|
5天前
|
安全 Java 数据安全/隐私保护
Java基础4-一文搞懂String常见面试题,从基础到实战,更有原理分析和源码解析!(二)
Java基础4-一文搞懂String常见面试题,从基础到实战,更有原理分析和源码解析!(二)
15 0
|
5天前
|
JSON 安全 Java
Java基础4-一文搞懂String常见面试题,从基础到实战,更有原理分析和源码解析!(一)
Java基础4-一文搞懂String常见面试题,从基础到实战,更有原理分析和源码解析!(一)
14 0