继Java 8系列之Lambda表达式之后,我们来了解Stream。Stream 是用函数式编程方式在集合类上进行复杂操作的工具,其集成了Java 8中的众多新特性之一的聚合操作,开发者可以更容易地使用Lambda表达式,并且更方便地实现对集合的查找、遍历、过滤以及常见计算等。
聚合操作
为了学习聚合的使用,在这里,先定义一个数据类:

现有一个List list里面有5个Studeng对象,假如我们想获取Sex=“G”的Student,并打印出来。如果按照我们原来的处理模式,必然会想到一个for循环就搞定了,而在for循环其实是一个封装了迭代的语法块。在这里,我们采用Iterator进行迭代:

整个迭代过程是这样的:首先调用iterator方法,产生一个新的Iterator对象,进而控制整
个迭代过程,这就是外部迭代 迭代过程通过显式调用Iterator对象的hasNext和next方法完成迭代
而在Java 8中,我们可以采用聚合操作:

首先,通过stream方法创建Stream,然后再通过filter方法对源数据进行过滤,最后通过foeEach方法进行迭代。在聚合操作中,与Labda表达式一起使用,显得代码更加的简洁。这里值得注意的是,我们首先是stream方法的调用,其与iterator作用一样的作用一样,该方法不是返回一个控制迭代的 Iterator 对象,而是返回内部迭代中的相应接口: Stream,其一系列的操作都是在操作Stream,直到feach时才会操作结果,这种迭代方式称为内部迭代。
外部迭代和内部迭代(聚合操作)都是对集合的迭代,但是在机制上还是有一定的差异:
迭代器提供next()、hasNext()等方法,开发者可以自行控制对元素的处理,以及处理方式,但是只能顺序处理;
stream()方法返回的数据集无next()等方法,开发者无法控制对元素的迭代,迭代方式是系统内部实现的,同时系统内的迭代也不一定是顺序的,还可以并行,如parallelStream()方法。并行的方式在一些情况下,可以大幅提升处理的效率。
Stream
如何使用Stream?
聚合操作是Java 8针对集合类,使编程更为便利的方式,可以与Lambda表达式一起使用,达到更加简洁的目的。
前面例子中,对聚合操作的使用可以归结为3个部分:
创建Stream:通过stream()方法,取得集合对象的数据集。
Intermediate:通过一系列中间(Intermediate)方法,对数据集进行过滤、检索等数据集的再次处理。如上例中,使用filter()方法来对数据集进行过滤。
Terminal通过最终(terminal)方法完成对数据集中元素的处理。如上例中,使用forEach()完成对过滤后元素的打印。
在一次聚合操作中,可以有多个Intermediate,但是有且只有一个Terminal。也就是说,在对一个Stream可以进行多次转换操作,并不是每次都对Stream的每个元素执行转换。并不像for循环中,循环N次,其时间复杂度就是N。转换操作是lazy(惰性求值)的,只有在Terminal操作执行时,才会一次性执行。可以这么认为,Stream 里有个操作函数的集合,每次转换操作就是把转换函数放入这个集合中,在 Terminal 操作的时候循环 Stream 对应的集合,然后对每个元素执行所有的函数。
Stream的操作分类
刚才提到的Stream的操作有Intermediate、Terminal和Short-circuiting:
Intermediate:map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 skip、 parallel、 sequential、 unordered
Terminal:forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、iterator
Short-circuiting:
anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 limit
惰性求值和及早求值方法
像filter这样只描述Stream,最终不产生新集合的方法叫作惰性求值方法;而像count这样最终会从Stream产生值的方法叫作及早求值方法。

如何判断一个操作是惰性求值还是及早求值,其实很简单,只需要看其返回值即可:如果返回值是Stream,那么就是惰性求值;如果返回值不是Stream或者是void,那么就是及早求值。上面的示例中,只是包含两步:一个惰性求值-filter和一个及早求值-count。
前面,已经说过,在一个Stream操作中,可以有多次惰性求值,但有且仅有一次及早求值。
创建Stream
- 我们有多种方式生成Stream:
- Stream接口的静态工厂方法(注意:Java8里接口可以带静态方法);
- Collection接口和数组的默认方法(默认方法,也使Java的新特性之一,后续介绍),把一个Collection对象转换成Stream
- 其他
- Random.ints()
- BitSet.stream()
- Pattern.splitAsStream(java.lang.CharSequence)
- JarFile.stream()
静态工厂方法
of
of方法,其生成的Stream是有限长度的,Stream的长度为其内的元素个数。
- of(T... values):返回含有多个T元素的Stream- of(T t):返回含有一个T元素的Stream
示例:
Stream integerStream = Stream.of(1, 2, 3);Stream stringStream = Stream.of("A");
generator
generator方法,返回一个无限长度的Stream,其元素由Supplier接口的提供。在Supplier是一个函数接口,只封装了一个get()方法,其用来返回任何泛型的值,该结果在不同的时间内,返回的可能相同也可能不相同,没有特殊的要求。
- generate(Supplier s):返回一个无限长度的Stream
- 这种情形通常用于随机数、常量的 Stream,或者需要前后元素间维持着某种状态信息的 Stream。
- 把 Supplier 实例传递给 Stream.generate() 生成的 Stream,默认是串行(相对 parallel 而言)但无序的(相对 ordered 而言)。
示例:

以上三种形式达到的效果是一样的,只不过是下面的两个采用了Lambda表达式,简化了代码,其实际效果就是返回一个随机值。一般无限长度的Stream会与filter、limit等配合使用,否则Stream会无限制的执行下去,后果可想而知,如果你有兴趣,不妨试一下。
iterate
iterate方法,其返回的也是一个无限长度的Stream,与generate方法不同的是,其是通过函数f迭代对给指定的元素种子而产生无限连续有序Stream,其中包含的元素可以认为是:seed,f(seed),f(f(seed))无限循环。
- iterate(T seed, UnaryOperator f)
示例:
Stream.iterate(1, item -> item + 1) .limit(10) .forEach(System.out::println);
// 打印结果:1,2,3,4,5,6,7,8,9,10
上面示例,种子为1,也可认为该Stream的第一个元素,通过f函数来产生第二个元素。接着,第二个元素,作为产生第三个元素的种子,从而产生了第三个元素,以此类推下去。需要主要的是,该Stream也是无限长度的,应该使用filter、limit等来截取Stream,否则会一直循环下去。
empty
empty方法返回一个空的顺序Stream,该Stream里面不包含元素项。
Collection接口和数组的默认方法
在Collection接口中,定义了一个默认方法stream(),用来生成一个Stream。

在Arrays类,封装了一些列的Stream方法,不仅针对于任何类型的元素采用了泛型,更对于基本类型作了相应的封装,以便提升Stream的处理效率。

示例:

其他
- Random.ints()
- BitSet.stream()
- Pattern.splitAsStream(java.lang.CharSequence)
- JarFile.stream()
Intermediate
Intermediate主要是用来对Stream做出相应转换及限制流,实际上是将源Stream转换为一个新的Stream,以达到需求效果。
concat
concat方法将两个Stream连接在一起,合成一个Stream。若两个输入的Stream都时排序的,则新Stream也是排序的;若输入的Stream中任何一个是并行的,则新的Stream也是并行的;若关闭新的Stream时,原两个输入的Stream都将执行关闭处理。
示例:
Stream.concat(Stream.of(1, 2, 3), Stream.of(4, 5)) .forEach(integer -> System.out.print(integer + " "));// 打印结果1 2 3 4 5
distinct
distinct方法以达到去除掉原Stream中重复的元素,生成的新Stream中没有没有重复的元素。

Stream.of(1,2,3,1,2,3) .distinct() .forEach(System.out::println);
// 打印结果:1,2,3
创建了一个Stream(命名为A),其含有重复的1,2,3等六个元素,而实际上打印结果只有“1,2,3”等3个元素。因为A经过distinct去掉了重复的元素,生成了新的Stream(命名为B),而B
中只有“1,2,3”这三个元素,所以也就呈现了刚才所说的打印结果。
filter
filter方法对原Stream按照指定条件过滤,在新建的Stream中,只包含满足条件的元素,将不满足条件的元素过滤掉。

示例:
Stream.of(1, 2, 3, 4, 5) .filter(item -> item > 3) .forEach(System.out::println);
// 打印结果:4,5
创建了一个含有1,2,3,4,5等5个整型元素的Stream,filter中设定的过滤条件为元素值大于3,否则将其过滤。而实际的结果为4,5。
filter传入的Lambda表达式必须是Predicate实例,参数可以为任意类型,而其返回值必须是boolean类型。

map
map方法将对于Stream中包含的元素使用给定的转换函数进行转换操作,新生成的Stream只包含转换生成的元素。为了提高处理效率,官方已封装好了,三种变形:mapToDouble,mapToInt,mapToLong。其实很好理解,如果想将原Stream中的数据类型,转换为double,int或者是long是可以调用相对应的方法。
示例:
Stream.of("a