Jav8中,在核心类库中引入了新的概念,流(Stream)。流使得程序媛们得以站在更高的抽象层次上对集合进行操作。
今天,居士将主要介绍Steam类中对应集合上操作的几个重要的方法。
1、 Steam举例
对使用Java的程序媛们,当需要处理集合里的每一个数据时,通常是使用迭代,再对每个返回的元素进行处理。比如:
int count = 0;
ArrayList<String> nameList = new ArrayList<>();
nameList.add("仁昌居士");
nameList.add("仁昌居士");
nameList.add("痕无羽");
nameList.add("羽无痕");
for (String name: nameList) {
if(name.equals("仁昌居士"))
count++;
}
尽管这段代码思想上 并不难理解,但是存在几个问题:
(1) 从代码量上来看,每一次的循环集合类,都需要重复写很多的样板代码。
(2)对于for循环写的代码块,有些程序媛可能很难理解其编写意图。需要阅读整个循环体后,才能有一定的理解。假设只有一个for循环,相对理解并不难,但是当出现多层嵌套循环,那理解所花费的成本就大幅度提升了。
分析一下for循环的实现原理,可知是通过调用了iterator()方法,产生了一个Iterator对象,通过while方法遍历的显式调用这个对象的hasNext()和next()方法。以实现需求。这种遍历过程叫做外部遍历,是一种串行化操作。
Iterator<String> iterator = nameList.iterator();
while(iterator.hasNext()){
String name = iterator.next();
if(name.equals("仁昌居士")){
count++;
}
}
注意事项:为什么对for循环叫他外部遍历而不是外部迭代的原因?可见另一篇文章:还未写,周末写。
相对于外部遍历,还有一种方法叫做内部遍历。通过内部遍历,将上述代码实现为:
long count = nameList.stream().filter(name -> name.equals("仁昌居士"))
.count();
上述代码实际是三步,第一步:nameList创建了一个Stream实例,第二步:用fliter操作符过滤找出为“仁昌居士”的name,并转换成另外一个Stream,第三步:把Stream的里面包含的内容按照某种算法来成型成一个值,代码中式用count操作符计算有几个这样的name。
2、 惰性求值和及早求值
通常,在Java中调有一个方法,计算机会随机执行相应的操作,比如通过println在终端上输出一条信息。Stream里的方法则有些不同。比如说:
nameList.stream().filter(name -> name.equals("仁昌居士"));
这行代码并没有通过fliter得到新的集合,只是对Stream进行了描述,这种方法叫做“惰性求值”方法,而之后的“.count()”使Stream产生了值的方法,叫做“及早求值”方法。
最好的验证方式就如下。
单纯的在filter中加入一条println语句:
nameList.stream()
.filter(name -> {
System.out.println(name);
return name.equals("仁昌居士");
});
运行结果是程序并没有输出对应信息。
再测试:在后面加入一个及早求值方法,如count(),将会得到输出结果。
nameList.stream()
.filter(name -> {
System.out.println(name);
return name.equals("仁昌居士");
})
.count();
想知道操作符是惰性求值操作符还是及早求值操作符,只需观察其返回值,如果返回值是Stream,则是惰性求值操作符;如果返回值是另一个类型或者是void,则是及早求值操作符。通过这种多个惰性求值操作符+一个及早求值操作符消费为结尾的链来得到想要的值,这个过程和建造者Builder模式很相似。建造者Builder模式就是通过使用一系列操作设置属性和配置,最后通过一个build方法,将对象真正创建出来。
3、 常用的Stream操作符
现在讲述几个比较常用的Stream API。
3.1 创建Stream操作符
3.1.1 of
Stream的of操作符,是将一组数据生成一个Stream。是一个惰性求值操作符。
Stream nameStream = Stream.of("仁昌居士","痕无羽","羽无痕");
3.1.2 generate
生成一个无限长度的Stream,其元素的生成是通过给定的Supplier(这个接口可以看成一个对象的工厂,每次调用返回一个给定类型的对象),也是一个惰性求值操作符。
Stream.generate(() -> Math.random());
生成一个无限长度的Stream,其中值是随机的。这个无限长度Stream是懒加载,一般这种无限长度的Stream都会配合Stream的limit()方法来用。
3.1.3 iterate
iterate操作符生成无限长度的Stream,和generator不同的是,其元素的生成是重复对给定的种子值(seed)调用用户指定函数来生成的。其中包含的元素可以认为是:seed,f(seed),f(f(seed))无限循环,也是惰性求值操作符
Stream.iterate(1, item -> item + 1).limit(10).forEach(System.out::println);
这段代码就是先获取一个无限长度的正整数集合的Stream,然后取出前10个打印。千万注意:使用limit方法,不然会无限打印下去。
3.2 转换Stream操作符
3.2.1 map
map操作符的作用就是将Stream中的每个值进行同一个操作的处理后,再将其转换为一个新的Stream,所以是惰性求值操作符。
List<String> list = Stream.of(1,2,3)
.map(integer -> String.valueOf(integer))
.collect(Collectors.toList());
看上面这段代码可知,通过map操作符和Lambda表达式将一个Integer类型的参数转成了一个String的返回值。参数和返回值直接不必是同一种类型,但是Lambda表达式,必须是Function接口(只包含一个参数的普通函数接口)的一个实例。
注意事项:用map操作符得到的还是Stream。
3.2.2 flatMap
flatMap操作符不同于map操作符将Stream中的值转换为新值,他能将多个Stream合成一个Stream,返回值也是Stream。是惰性求值操作符。
ArrayList<Integer> arrayList1 = new ArrayList<>();
arrayList1.add(1);
arrayList1.add(2);
ArrayList<Integer> arrayList2 = new ArrayList<>();
arrayList2.add(3);
arrayList2.add(4);
List<Integer> list3 = Stream.of(arrayList1,arrayList2)
.flatMap(numbers -> numbers.stream())
.collect(Collectors.toList());
通过stream()方法,将每个ArrayList转换成了Stream对象,其余部分由flatMap操作符处理,得到的Stream是Stream.of(1,2,3,4)。
3.2.3 distinct
distinct操作符,是对Stream中包含的元素进行去重操作(去重逻辑依赖元素的equals方法),新生成的Stream中没有重复的元素。是惰性求值操作符。
Stream stream = Stream.of(1, 2, 3, 4,1,2,2,3,4)
.distinct();
得到的Stream里面的元素只有1,2,3,4四个。重复的都被去掉了。
3.2.4 filter
fliter操作符,上文已经提及过了,是用于过滤的惰性求值操作符。
List<Integer> list = Stream.of(1,2,3)
.filter(integer -> integer >1)
.collect(Collectors.toList());
和map操作符相似,filter操作符接受一个函数为参数,该函数通过Lambda表达式表示,如这段代码,Lambda表达式将会对大于1的返回true,否则返回false。这段代码就是通过filter操作符过滤选择Lambda表达式返回值为true的元素保留生成新的Stream,并通过collect操作符得到符合要求的List。
3.2.5 peek
peek生成一个包含原Stream的所有元素的新Stream,同时会提供一个消费函数(Consumer实例),当最终用及早求值操作符消费此Stream时,新Stream每个元素都会执行给定的消费函数。是惰性求值操作符。
nameList.stream()
.filter(name -> name.equals("仁昌居士"))
.peek(name -> System.out.println(name))
.collect(Collectors.toList());
3.2.6 limit
limit对一个Stream进行截断操作,获取其前N个元素,如果原Stream中包含的元素个数小于N,那就获取其所有的元素,是惰性求值操作符。
Stream stream = Stream.of(1, 2, 3, 4,5,6,7,8,9,10)
.limit(3);
得到的新的Stream的元素只有前3个。后面的被截断了。
3.2.7 skip
返回一个跳过原Stream的前N个元素后剩下元素组成的新Stream,如果原Stream中包含的元素个数小于N,那么返回空Stream,是惰性求值操作符。
Stream stream = Stream.of(1, 2, 3, 4,5,6,7,8,9,10)
.skip(3);
得到的新的Stream的元素只有后7个。前面的3个被跳过不要了。
3.3 成型(Reduce)Stream操作符
成型(Reduce)Stream操作符和.reduce()操作符是两个东西。
成型(Reduce)是个概念,我将其理解为将Stream在经过多次转换操作后确定最终成型得到一个特定的非Stream的结果。
而成型(Reduce)Stream操作符就是对Stream反复使用某个合并操作,把序列中的元素合并成一个整合结果的操作符。比如:max操作符、min操作符、sum操作符、count操作符、reduce()操作符、collect操作符等等。
注意事项:其中collect操作符和其他几个操作符不同。他最终成型的结果是一个可变的容器,比如Collection或者StringBuilder。
3.3.1 max和min
Stream中进行大小比较是比较常用的操作,所以有了max和min操作符,返回值类型是Optional,这是Java8防止出现NPE的一种可行方法,后面的文章会详细介绍,这里就简单的认为是一个容器,其中可能会包含0个或者1个对象。。
查找Stream中的最大或最小元素,就要考虑是用什么作为排序的指标。
Integer integer3 = Stream.of(1, 2, 3, 4)
.min((x,y) -> x.compareTo(y))
.get();
通过比较两个对象的值的大小,来得到最小值。对于这个指标,也可以通过Comparator对象。
Integer integer = Stream.of(1, 2, 3, 4)
.min(Comparator.naturalOrder())
.get();
max和min方法同理,意思也一目了然,所以不用过多描述,都是及早求值操作符。
3.3.2 sum
sum操作符不是所有的Stream对象都有的,只有IntStream、LongStream和DoubleStream是实例才有。
int sum = IntStream.of(1, 2, 3, 4,5,6,7,8,9,10)
.sum ();
sum为55。求和的及早求值操作符。
3.3.3 count
count操作符不是求Stream中元素的数量。
long count= Stream.of(0,1, 2, 3, 4,5,6,7,8,9)
.count();
count为10。求元素个数的及早求值操作符。
3.3.4 reduce
reduce操作符是及早求值操作符,接受一个元素序列为输入,反复使用某个合并操作,把序列中的元素合并成一个汇总的结果,其生成的值不是随意的,而是根据指定的计算模型。像之前的count、min、max操作符都是reduce操作。
reduce方法有三个override的方法。
Optional<T> reduce(BinaryOperator<T> accumulator);
T reduce(T identity, BinaryOperator<T> accumulator);
<U> U reduce(U identity,BiFunction<U, ? super T, U> accumulator,BinaryOperator<U> combiner);
先来看reduce方法的第一种形式,其方法定义如下:
Optional<T> reduce(BinaryOperator<T> accumulator);
接受一个BinaryOperator类型的参数,在使用的时候我们可以用lambda表达式来。
Stream.of(1,2,3,4,5,6,7,8,9,10).reduce((sum, item) -> sum + item).get();
结果都为55。
可以看到reduce方法接受一个函数,这个函数有两个参数,第一个参数是上次函数执行的返回值(也称为中间结果),第二个参数是stream中的元素,这个函数把这两个值相加,得到的和会被赋值给下次执行这个函数的第一个参数。要注意的是:第一次执行的时候第一个参数的值是Stream的第一个元素,第二个参数是Stream的第二个元素。这个方法返回值类型是Optional。
再来看reduce方法的第二种形式,其方法定义如下:
T reduce(T identity, BinaryOperator<T> accumulator);
与第一种变形相同的是都会接受一个BinaryOperator函数接口,不同的是其会接受一个identity参数,用来指定Stream循环的初始值。如果Stream为空,就直接返回该值。另一方面,该方法不会返回Optional,因为该方法不会出现null。
Stream.of(1,2,3,4,5,6,7,8,9,10).reduce(1, (sum, item) -> sum + item)).get();
结果都为56。
变形1,未定义初始值,从而第一次执行的时候第一个参数的值是Stream的第一个元素,第二个参数是Stream的第二个元素。
变形2,定义了初始值,从而第一次执行的时候第一个参数的值是初始值,第二个参数是Stream的第一个元素。
<U> U reduce(U identity,BiFunction<U, ? super T, U> accumulator,BinaryOperator<U> combiner);
对于第三种变形,我们先看各个参数的含义,第一个参数类型是实际返回实例的数据类型,同时其为一个泛型也就是意味着该变形的可以返回任意类型的数据,第二个参数累加器accumulator,可以使用二元?表达式(即二元lambda表达式),声明你在u上累加你的数据来源t的逻辑,例如(u,t)->u.sum(t),此时lambda表达式的行参列表是返回实例u和遍历的集合元素t,函数体是在u上累加t,第三个参数组合器combiner,同样是二元?表达式,(u,t)->u, 是用来处理并发操作的。因为Stream是支持并发操作的,为了避免竞争,对于reduce线程都会有独立的result,combiner的作用在于合并每个线程的result得到最终结果。这也说明了了第三个函数参数的数据类型必须为返回数据类型了。代码并不好举例,先不距离,在以后的讲解中会提及。
3.3.5 collect
collect操作符:是一个及早求值操作符。它可以把Stream中的要有元素收集到一个结果容器中(比如Collection)。先看一下最通用的collect方法的定义(还有其他override方法)。
<R> R collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner);
先来看看这三个参数的含义:Supplier supplier是一个工厂函数,用来生成一个新的容器;BiConsumer accumulator也是一个函数,用来把Stream中的元素添加到结果容器中;BiConsumer combiner还是一个函数,用来把中间状态的多个结果容器合并成为一个(并发的时候会用到)。
List<Integer> numsWithoutNull = Stream.of(1,2,3,4,5,6,7,8,9,10)
.collect(() -> new ArrayList<Integer>(),(list, item) -> list.add(item),(list1, list2) -> list1.addAll(list2));
上面这段代码就是把一个元素是Integer类型的List收集到一个新的List中。进一步看一下collect方法的三个参数,都是lambda形式的函数。
第一个函数生成一个新的ArrayList实例;
第二个函数接受两个参数,第一个是前面生成的ArrayList对象,二个是stream中包含的元素,函数体就是把stream中的元素加入ArrayList对象中。第二个函数被反复调用直到原stream的元素被消费完毕;
第三个函数也是接受两个参数,这两个都是ArrayList类型的,函数体就是把多个ArrayList容器合并成为一个。
但是上面的collect方法调用有些复杂了,有更简单的override方法,其依赖Collector。
<R, A> R collect(Collector<? super T, A, R> collector);
进一步,Java8还给我们提供了Collector的工具类–Collectors,其中已经定义了一些静态工厂方法,比如:Collectors.toCollection()收集到Collection中, Collectors.toList()收集到List中和Collectors.toSet()收集到Set中,等等。这样的静态方法还有很多,这里就不一一介绍了,大家可以直接去看文档。下面看看使用Collectors对于代码的简化:
List<Integer> numsWithoutNull = Stream.of(1,2,3,4,5,6,7,8,9,10)
.collect(Collectors.toList());
这段代码将of()操作符得到的Stream,用collect(Collectors.toList())操作符从Stream中生成一个List。
4、 性能问题
完成了上述的讲解,会发现在使用操作符时,会出现对于一个Stream进行多次转换操作,每次都对Stream的每个元素进行转换,而且是执行多次,这样时间复杂度就是一个for循环里把所有操作都做掉的N(转换的次数)倍啊。其实不是这样的,转换操作都是lazy的,多个转换操作只会在成型(Reduce)操作的时候融合起来,一次循环完成。我们可以这样简单的理解,Stream里有个操作函数的集合,每次转换操作就是把转换函数放入这个集合中,在成型(Reduce)操作的时候循环Stream对应的集合,然后对每个元素一次性执行所有的操作。
5、总结
对于Stream,单纯的书面理解是很难明白的,码字看方法才是最好的学习方法。所以我的御姐儿,你还是多码码代码吧。本居士很忙的啊。