Java|Java8新特性(二) Stream(流)

引例 还记得上一篇文章中的农场主吗?这一次农场主又提出了新的需求——找出所有果园里150g以上的绿苹果,除此之外还要按照重量从大到小给苹果排序。然而这并难不倒掌握Lambda表达式的我们,下面就用代码实现他的需求:

public class Apple {private String color; private int weight; public Apple(String color, int weight) { this.color = color; this.weight = weight; }public String getColor() { return color; } public void setColor(String color) { this.color = color; } public int getWeight() { return weight; } public void setWeight(int weight) { this.weight = weight; }@Override public String toString() { return "Apple [color=" + color + ", weight=" + weight + "]"; }}

public class FindApple1 { // 果园 public static List orchard = Arrays.asList(new Apple("green", 150), new Apple("green", 200), new Apple("yellow", 150),new Apple("red", 170)); public static void main(String[] args) { List basket = appleFilterThenSort(orchard, apple -> "green".equals(apple.getColor()) ? (apple.getWeight() > 150 ? true : false) : false); basket.forEach(System.out::println); } private static List appleFilterThenSort(List orchard, Predicate predicate) { List temp = new ArrayList<>(); for (Apple apple : orchard) { if(predicate.test(apple)) { temp.add(apple); } } temp.sort((o1,o2) -> o2.getWeight() - o1.getWeight()); return temp; }}

尽管我们已经尽可能的使用Lambda表达式,但程序中的代码量仍然不少,那么还有方法可以让我们的程序变得更加简洁呢?答案是肯定的,下面是使用流修改后的代码的样子:
public class FindApple2 { public static void main(String[] args) { List basket = FindApple1.orchard.stream() .filter(apple -> "green".equals(apple.getColor()) ? (apple.getWeight() > 150 ? true : false) : false) .sorted((o1, o2) -> o2.getWeight()- o1.getWeight()) .collect(Collectors.toList()); basket.forEach(System.out::println); } }

使用流之后我们的代码变得更加精简、更加易读,即使是没有接触过流的人,也能大概明白代码所表达的含义:首先过滤数据,然后对数据进行排序,最后将收集到的数据返回。换而言之,使用流之后代码所表达的含义发生了变化——从命令式编程变成函数式编程。
命令式编程:制定做一件事情的步骤(FindApple1)——创建新集合,遍历原始集合并找出符合条件的数据添加到新集合中,对新集合排序,将新集合作为结果返回。
函数式编程:描述一件事情怎么做(FindApple2)——将符合条件的数据排序之后收集到集合中返回。

什么是流 流到底是什么呢?JavaDoc上给出的定义是这样的:
A sequence of elements supporting sequential and parallel aggregate operations.
流是一个支持串行和并行的聚合操作的元素序列。
《Java8实战》中给流下了这样的定义:
从支持数据处理操作的源生成的元素序列。
纵观二者, 他们都认为流是一个元素序列。提到元素序列我们很容易就可以联想到集合,同为元素序列,集合和流之间是什么关系呢?区别在于,集合负责的是元素的存储和访问,流的目的则在于计算。
流可以认为是对集合功能上的增强,能对集合对象实现更高效、更便利的操作。但是流不是数据结构,流本身不储存数据,只是从源(集合是流使用最多的源,下面会介绍其他的源)中获取数据, 并进行相应的计算——对流的操作会生成一个结果,不会修改数据源。
上面所说的都是理论上的流,具体到代码中,流指的就是Stream对象。
引例部分的最后这样说过:使用流之后代码所表达的含义发生了变化。其实流操作的参数含义也发生了变化:在Java8之前参数传递的是值,而Java8中流操作将Lambda表达式作为参数传递,传递的是一种行为。

使用流 流的使用一般包括下面三件事情:
  1. 一个数据源(如集合)
  2. 零个或多个中间操作(中间操作会返回一个新的流)
  3. 一个终止操作(终止操作会关闭流)

流是一次性的 从名字上来看,流是河流的意思,而流使用起来正如流水一般,一去不返。是的,流是一次性的,每个流只能使用一次。
public class TestStream1 { public static void main(String[] args) { Stream stream = Stream.generate(Math::random).limit(10); stream.forEach(System.out::println); //stream.forEach(System.out::println); // 错误: 流只能使用一次 } }

既然流是一次性的,那么在FindApple2中为什么可以对流进行多次操作(filter、sorted、collect)呢?这就和流是一次性的说法相矛盾了。这是因为流的中间操作会返回一个新的流,这样一来我们就可以将多个中间操作串联起来,实现聚合操作。
部分中间操作举例:
Stream filter(Predicate predicate);
Stream sorted(Comparator comparator);

惰性求值与及早求值 惰性求值(Lazy Evaluation)是在需要时才进行求值的计算方式,与惰性求值相对立的就是及早求值(Eager Evaluation),及早求值需要立即求值。
流的中间操作属于惰性求值,而终止操作属于及早求值。
由于流本身是不存储数据的,所以如果使用流时只有中间操作计算数据,而没有终止操作返回计算结果,使用流就是毫无意义的。因此如果使用流时没有终止操作(及早求值),那么流的中间操作(惰性求值)都不会被执行。看下面一个例子:
public class TestStream2 { public static void main(String[] args) { // filter和map是中间操作 List data = https://www.it610.com/article/Arrays.asList("hello", "world", "hello world"); data.stream().filter(item -> { System.out.println("filter invoked"); return item.length() > 5; }).map(item -> { System.out.println("map invoked"); return item + " java"; }); } }

运行上面的程序,并不会看到任何的打印结果。这是因为使用流时只使用中间操作(filter和map)而没有终止操作,所以流的中间操作都不会执行。
惰性求值最重要的好处是它可以构造一个无限的流,而这是集合做不到的。集合中的每个元素只有先计算出来,才能添加到集合中去,因此集合只能存储有限个元素。
List list = new ArrayList<>();
list.add(1);
list.add(2);
......// 集合中只能存放有限个元素
Stream.generate(Math::random); // 一个无限流

内部迭代与外部迭代 在流出现之前的,我们对集合所做的迭代(如for-each)都是外部迭代,而流使用的自然就是内部迭代。区分外部迭代和内部迭代很容易,外部迭代是显式的,你可以清楚的看到完整的迭代过程;相反内部迭代则是隐式的,你看不到迭代的过程。
public class TestStream3 { public static void main(String[] args) { List data = https://www.it610.com/article/Arrays.asList(1,2,3,4,5,6); // 外部迭代,可以看到迭代的过程 for (Integer item : data) { if (item> 3) System.out.println(item); } // 内部迭代,看不到迭代的过程 data.stream().filter(item -> item > 3).forEach(System.out::println); } }

使用外部迭代时,整个迭代过程全部都是由我们自己搭建的。我们编写的迭代逻辑和集合之间的关联很弱,迭代逻辑始终游离在集合之外。
Java|Java8新特性(二) Stream(流)
文章图片

而使用内部迭代时,我们并不需要去搭建整个迭代过程,只需要将一些关键的迭代逻辑通过Lambda表达式传递给流就可以了。所以内部迭代的本质是流框架,流框架已经事先为我们搭建好了完整的迭代过程,它会将流从源中获取到的元素和我们编写的迭代逻辑整合到一起。
Java|Java8新特性(二) Stream(流)
文章图片

同时,使用外部迭代是比较难并行化的,一旦你选择了外部迭代,那么你就需要自己管理所有的并行问题,而使用内部迭代则可以自动为你实现并行(并行流)。

创建流 前面说了很多理论上的东西,接下来就让我们实际创建流。创建流的方法:
1、通过集合创建流:集合根接口Collection中定义有默认方法stream(),所以Collection的所有直接或间接实现类都会自动继承该方法,调用集合对象的stream()方法就可以创建流。
2、通过给定值创建流:Stream接口中提供静态方法of(),该方法接收一到任意个参数,调用该方法就可以创建流。当然你也可以通过Stream接口提供的静态方法empty()来创建一个空流。
3、通过数组创建流:Arrays类提供静态方法stream()创建一个流,该方法接受一个数组作为参数。
4、通过函数创建流:Stream接口提供两个静态方法iterate()和generate(),通过这两个方法可以创建无限流(一般会同时调用limit()方法对无限流加以限制)。
  • iterate():该方法接受两个参数——一个初始值和一个依次应用在每个产生的新值上的Lambda表达式。
  • generate():该方法只接受一个Supplier类型的Lambda表达式用于源源不断的提供新值。
下面通过一个例子来演示上述的方法:
public class TestStream4 { public static void main(String[] args) { createStreamByCollection().forEach(System.out::println); createStreamByValues().forEach(System.out::println); createStreamByArrays().forEach(System.out::println); createStreamByIterator().forEach(System.out::println); createStreamByGenerate().forEach(System.out::println); } // 通过集合创建流 private static Stream createStreamByCollection() { List list = Arrays.asList("hello", "world"); return list.stream(); } // 通过给定值创建流 private static Stream createStreamByValues() { return Stream.of("hello", "world"); } // 通过数组创建流 private static Stream createStreamByArrays() { String[] arr = {"hello", "world"}; return Arrays.stream(arr); } // 通过iterator创建流,无限流 private static Stream createStreamByIterator() { return Stream.iterate(0, i -> i + 2).limit(10); // 这里限制生成10个 } // 通过generate创建流,无限流 private static Stream createStreamByGenerate () { return Stream.generate(Math::random).limit(10); // 这里限制生成10个 } }


流操作 流操作可以分为两类:中间操作(intermediate operation)和终止操作(terminal operation)。
通过操作的返回值就可以区分这两类操作:如果操作返回一个Stream对象,那么该操作就是中间操作;反之,该操作就是终止操作。
中间操作 过滤:filter
该操作可以过滤掉不符合条件的元素,并返回一个新的由符合条件的的元素组成的流。filter()方法使用Predicate接口作为参数:
Stream filter(Predicate predicate);
public class TestStream5 { public static void main(String[] args) { List data = https://www.it610.com/article/Arrays.asList(1,5,4,2,3); // filter操作并没有改变流中元素的顺序 Stream stream = data.stream().filter(i -> i%2 ==0); stream.forEach(System.out::println); } }

打印结果如下:
4
2
从打印结果可以发现,filter操作并没有改变流中元素的顺序。
映射:map
编码时经常需要这样的操作:从一堆数据中挑选我们需要的数据,比如从表中选择一列。这种两个元素集之间的对应关系就是映射,流也给我们提供了相关的映射操作。
该操作会对流中元素进行映射,并返回一个新的由映射产生元素组成的流。map()方法使用Function接口作为参数:
Stream map(Function mapper);
public class TestStream6 { public static void main(String[] args) { List data = https://www.it610.com/article/Arrays.asList(new Apple("red", 150), new Apple("green", 170), new Apple("yellow", 200)); Stream appleStream = data.stream(); // 流的类型发生了变化Stream -> Stream Stream stringStream = appleStream.map(apple -> apple.getColor()); stringStream.forEach(System.out::println); } }

打印结果如下:
red
green
yellow
例子中我们通过map()方法将流中的Apple类型元素中映射成String类型元素,因此流的类型也发生了变化。但是只靠map()方法真的可以映射出所有我们需要的元素吗?
假设存在列表["hello","world"],需要把列表中的数据处理这样["h","e","l", "o","w","r","d"],应该怎么做呢?可能你会这样做:
public class TestStream7 { static String[] arr = {"hello", "world"}; public static void main(String[] args) { // 调用map()方法之后,流中元素类型为数组类型 Stream stream = Arrays.stream(arr).map(i -> i.split("")).distinct(); stream.forEach(System.out::println); } }

用数组记录需要处理的数据,通过该数组构建一个流,然后对流中元素进行映射操作——分割字符串,最后将流中元素去重就可以了。这样真的可以吗?
通过打印结果就可以发现上面的程序是无法完成任务的。打印结果如下:
[Ljava.lang.String; @53d8d10a
[Ljava.lang.String; @e9e54c2
从打印结果和代码都可以看出来,调用map()方法之后得到的流中的元素是数组类型,而不是我们想要的字符串类型,所以map()方法并没有实现我们想要的效果:
调用map()方法之后流中的元素: ["h","e","l","l","o"], ["w","o","r","l","d"]
我们想要的流中元素:"h","e","l","l","o","w","o","r","l","d"
所以这里我们还需要用到另一种映射操作——flatMap:
Stream flatMap(Function> mapper);
public class TestStream8 { public static void main(String[] args) { // 调用map()方法后,流的类型是数组类型 Stream stream = Arrays.stream(TestStream7.arr).map(i -> i.split("")); // 对流中数组进行扁平化处理 Stream flatmapStream = stream.flatMap(Arrays::stream).distinct(); flatmapStream.forEach(System.out::print); } }

打印结果如下:
helowrd
flatMap()方法也使用Function接口作为参数,只是调用该接口的返回值类型为Stream。flatMap操作将该接口应用于此流中的每个元素并生成多个映射流,然后将此流中被映射的元素替换成每个映射流中所有元素(每个映射流会在完成替换之后关闭),最后该操作返回一个新的由此流中替换之后的元素组成的流(不是返回此流,每个流只能使用一次)。
Java|Java8新特性(二) Stream(流)
文章图片

flat即扁平化,flatMap操作所做的事情就是将需要映射的数据扁平化。简单来说,flatMap操作会将流中每个元素都转换成一个流,然后将所有转换的流连接成一个新的流。
下面提供另一个例子来理解该方法:
public class TestStream9 { public static void main(String[] args) { List data1 = Arrays.asList("hello! ","hi! ","你好! "); List data2 = Arrays.asList("张三","李四","王五 ","赵六"); // 将两个流中的元素拼接 data1.stream() .flatMap(item1 -> data2.stream().map(item2 -> item1 + item2 + "。 ")) .forEach(System.out::print); } }

打印结果如下:
hello! 张三。 hello! 李四。 hello! 王五 。 hello! 赵六。 hi! 张三。 hi! 李四。 hi! 王五 。 hi! 赵六。 你好! 张三。 你好! 李四。 你好! 王五 。 你好! 赵六。
其他中间操作
除了上述两种主要的中间操作,Stream还提供了一些其他的中间操作:
Stream distinct();
Stream sorted();
Stream sorted(Comparator comparator);
Stream skip(long n);
Stream limit(long maxSize);
这些操作作用如下:
  1. distinct:去重
  2. sorted:排序(重载方法,一个是空参方法,另一个接收Comparator作为参数)
  3. skip:跳过指定个元素
  4. limit(short-circuiting):截取指定个元素
public class TestStream11 { public static void main(String[] args) { List data = https://www.it610.com/article/Arrays.asList(1,5,4,2,3,2,4,5,2,1); // distinct: 去重 // sorted:排序(可传入或不传比较器) data.stream().distinct().sorted(Integer::compare).forEach(System.out::print); System.out.print(System.lineSeparator()); // 换行 // skip: 跳过指定个数元素 data.stream().skip(5).forEach(System.out::print); System.out.print(System.lineSeparator()); // limit: 截取指定个元素 data.stream().limit(3).forEach(System.out::print); } }

打印结果如下:
12345
24521
154
终止操作 匹配:match(short-circuiting)
Stream提供三种match操作,如下所示:
boolean allMatch(Predicate predicate);
boolean anyMatch(Predicate predicate);
boolean noneMatch(Predicate predicate);
这三个方法都将Predicate接口作为参数,返回一个布尔值表示是否匹配。其具体含义如下:
  • allMatch():流中元素全部符合条件,返回true,否则返回false
  • anyMatch():流中存在任何一个元素符合条件,返回true,否则返回false
  • noneMatch():流中元素全都不符合条件,返回true,否则返回false
下面演示三种match操作:
public class TestStream12 { static List data = https://www.it610.com/article/Arrays.asList(1,2,3,4,5,6,7,8,9); public static void main(String[] args) { // 流中所有元素大于0 boolean allMatch = data.stream().allMatch(i -> i > 0); System.out.println(allMatch); // 流中任意元素大于10 boolean anyMatch = data.stream().anyMatch(i -> i > 10); System.out.println(anyMatch); // 流中没有元素大于10 boolean noneMatch =data.stream().noneMatch(i -> i > 10); System.out.println(noneMatch); } }

打印结果如下:
true
false
true
查找:find(short-circuiting)
查找操作有两种:
Optional findAny();
Optional findFirst();
需要注意的是这两个方法的返回值是Optional类型, 其作用如下:
  • findAny():找到流中任意一个元素
  • findFirst():找到流中第一个元素
具体看下面的例子:
public class TestStream13 { public static void main(String[] args) { List data = https://www.it610.com/article/TestStream12.data; // 找到流中任意元素 Optional result1 = data.stream().filter(i -> i >5).findAny(); result1.ifPresent(System.out::println); // 找到流中第一个元素 Optional result2 = data.stream().filter(i -> i >5).findFirst(); System.out.println(result2.orElse(-1)); } }

打印结果如下:
6
6
两个方法的调用结果一样。对于这个结果可能你会觉得只是一个巧合,于是我把findAny()方法在while循环中调用了1000次,每一次的调用结果都是6。这样的结果还是巧合吗?
其实这两个方法的区别体现在并行上。在串行流中调用findAny()方法和调用findFirst()方法并没有什么区别,都会取得流中第一个元素。但是在并行流中调用findAny()方法就会取得多个线程计算结果中的任意一个元素,而调用findFirst()方法则会严格保证取得多个线程计算结果中的一个元素。
所以如果你并不在意返回的元素是哪个,最好使用findAny()方法,因为它在并行流中的限制较少。
归约:reduce
归约是把流中元素汇聚成一个值的操作,该操作对应Stream接口中的reduce()方法。Stream中存在三个重载的reduce()方法:
Optional reduce(BinaryOperator accumulator);
T reduce(T identity, BinaryOperator accumulator);
U reduce(U identity,BiFunction accumulator,BinaryOperator combiner);
这三个reduce()方法都使用BinaryOperator接口作为参数:
public interface BinaryOperator extends BiFunction
BinaryOperator是一种特殊的BiFunction接口,更准的说,是三个泛型全都相同的BiFunction接口。这也不难理解,因为归约操作是将多个值合并成一个值的操作,所以操作的肯定是相同类型的数据,那么返回值也应该是同类型的数据。
下面演示前两种的reduce()方法,:
public class TestStream14 { public static void main(String[] args) { List data = https://www.it610.com/article/TestStream12.data; // 求最小值 Optional result = data.stream().reduce(Integer::min); result.ifPresent(System.out::println); // 求和 Integer result2 = data.stream().reduce(0, Integer::sum); System.out.print(result2); } }

这两个reduce()方法的区别在于:接收两个参数的reduce()方法将第一个参数作为返回值的初始值,这样的话即使是一个空的流调用该方法,返回值也不会是null(返回值是初始值);而接收一个参数的reduce()方法,调用的结果有可能是一个null,该方法的返回值类型是Optional。
接收三个参数的reduce()方法,参数combiner和并行相关。在串行流中combiner并不会被调用;而在并行流中combiner会用于将多个线程产生的汇聚结果进行合并,并作为最终的汇聚结果。
聚合:collect
聚合操作可以将流中元素收集整理后返回。该操作对应Stream接口中的collect()方法,Stream提供两个重载的collect()方法。
R collect(Supplier supplier,BiConsumer accumulator, BiConsumer combiner);
R collect(Collector collector);
第一种collect()方法接收三个参数,作用如下:
  • supplier:提供结果容器
  • accumulator:将流中元素添加到结果容器中
  • combiner:合并两个结果容器(并行流使用,合并多个线程产生的结果容器)
看下面一个例子:
public class TestStream15 { public static void main(String[] args) { Stream stream = Stream.of("hello", "world", "hello world"); ArrayList result = stream .collect(ArrayList::new, ArrayList::add, ArrayList::addAll); System.out.println(result); } }

打印结果如下:
[hello, world, hello world]
例子中调用collect()方法后执行操作是这样的:创建结果容器——ArrayList,调用结果容器的add()方法将流中元素添加到结果容器中,返回结果容器(虽然我们给出了合并多个结果容器的方法——addAll(),但是由于是串行流,该方法并没有被调用)。
不难发现,collect()方法的三个参数是需要组合使用的。既然如此,完全可以将这些参数抽取成一个接口。于是,就有了第二种collect()方法——将Collector接口作为参数。
Collector接口中不仅包含了supplier、accumulator、combiner,还增加了finisher和characteristics,可以让聚合操作变得更加强大。你可以自定义Collector实现类,也可以使用Collectors工厂类中提供的Collector实现。我们在FindApple2中使用的第二种collect()方法。

流的拆装箱 相信大家都知道Integer和int的区别,一个引用类型数据所占用的内存会比一个基本类型数据大出很多,这其中的差距会随着数据量的增加而愈发明显。Stream只能处理引用类型数据,所以使用流处理数据时会面临内存开销的问题。
因此除了Stream,JDK8还提供了IntStream、LongStream、DoubleStream三种处理基本类型数据的Stream,以减少内存上的开销。通过mapTo方法(包括mapToInt、mapToLong和mapToDouble)我们就可以将引用类型流转化成基本类型流。下面通过一个例子演示两种不同类型流处理数据的用时差距:
public class TestUnboxed { public static void main(String[] args) { List data = https://www.it610.com/article/Arrays.asList(new Integer[] {1,2,3,4,5,6,7}); // Integer数据流求和 long s1 = System.currentTimeMillis(); Integer result = data.stream().filter(i -> i > 3).reduce(0, Integer::sum); long e1 = System.currentTimeMillis(); System.out.println("Stream result: " + result + ", used time: " +(e1 - s1)); // int数据流求和 long s2 = System.currentTimeMillis(); IntStream intstream = data.stream().mapToInt(i -> i.intValue()); int sum = intstream.filter(i -> i > 3).sum(); long e2 = System.currentTimeMillis(); System.out.println("IntStream result: "+ sum + ", used time: " + (e2 - s2)); } }

打印结果如下(不同的机器会有略微的差别):
Stream result: 22, used time: 86
IntStream result: 22, used time: 3
通过打印结果可以发现,引用类型流和基本类型流处理数据的用时相差了几十倍。因此处理数据量比较大的时候,一定要记得将引用类型流拆箱为基本类型流再进行运算。
既然可以拆箱那么就一定可以装箱,基本类型流也可以类型提升为引用类型流。下面提供两种做法:
  1. 调用boxed()方法,将流中基本类型数据装箱为对应的引用类型数据(其底层还是调用了mapToObj()方法)
  2. 调用mapToObj()方法,将流中基本类型数据映射成指定的数据类型
public class TestBoxed { public static void main(String[] args) { // 数据装箱 IntStream intStream = IntStream.rangeClosed(1, 1000); Stream stream1 = intStream.boxed(); // 数据映射 DoubleStream doubleStream = DoubleStream.generate(Math::random); Stream stream2 = doubleStream.mapToObj(Double::new); } }


流的短路机制 看这样一个例子:
public class TestShortCircuiting1 { static List data = https://www.it610.com/article/Arrays.asList("hello","world","hello world"); public static void main(String[] args) { data.stream() .mapToInt(item -> { System.out.print(item + ","); return item.length(); }) .filter(item -> { System.out.print(item + ","); return item == 5; }) .findFirst() .ifPresent(System.out::print); } }

可能在你看来,上面的代码中流操作是这样进行的:
  1. 遍历流中所有元素并进行mapToInt操作;
  2. 遍历mapToInt操作返回的流中的所有元素并进行filter操作;
  3. 遍历filter操作返回的流中所有元素并进行findFirst操作;
  4. 打印findFirst操作返回的结果。
因此理想中控制台打印的结果应该是这样的:
hello,world,hello world,5,5,11,5
然而结果真的是这样吗?眼见为实!运行程序,打印结果如下:
hello,5,5
所以我们之前所做的推论都是错误的。
真实的流操作是这样运行的:只对源中的所有元素进行一次遍历,遍历过程中对每一元素应用所有流操作(包括中间操作和终止操作)。看到这里你可能会说:即使是流操作是这样进行的,控制台的也应该打印所有的元素。
但是,对元素应用流操作时存在短路机制——如果对元素应用的流操作中存在短路操作(short-circuiting operation)就会触发短路机制。短路这一概念相信你肯定不陌生,我们在编程中经常会碰到。例如||和&&运算符:
A || B:若A成立,则B不会被执行
A && B:若A不成立,则B不会被执行
findFirst操作相当于流操作中||运算符。对流中第一个元素“hello”应用所有流操作之后,findFirst操作就触发了短路机制,流中其他的元素就不会再遍历。
那么哪些流操作是短路操作呢?在介绍流操作部分,标有short-circuiting的操作都是短路操作。短路操作包括查找(find)、匹配(match)和截取(limit)。
我们将代码稍作修改就可以看到完全不同的打印结果:
public class TestShortCircuiting2 { public static void main(String[] args) { TestShortCircuiting1.data.stream() .mapToInt(item -> { System.out.print(item + ","); return item.length(); }) .filter(item -> { System.out.print(item + ","); return item == 11; })// 修改过滤条件 .findFirst() .ifPresent(System.out::print); } }

打印结果如下:
hello,5,world,5,hello world,11,11

参考:
《java8 实战》
https://www.ibm.com/developerworks/cn/java/j-lo-java8streamapi/index.html
https://segmentfault.com/a/1190000015806792
https://www.cnblogs.com/webor2006/p/8302401.html
【Java|Java8新特性(二) Stream(流)】https://segmentfault.com/q/1010000004944450

    推荐阅读