在前面的章节(Java JDK1.8 核心特性详解------Lambda表达式与方法引用),我们讲述了行为参数化以及Lambda表达式,在下面几篇文章里,我们会学习Stream的使用。
流是什么?
Stream流是 Java API的心成员,它允许你使用声明的方式处理数据集合。我们可以把流当作一种更加高级的迭代器。通过流我们可以更加方便的顺序或者并行的处理集合。下面将用例子让你先感受一下Stream的便利。假如我们要在一个List集合中筛选出年龄小于40岁的人,并且按照年龄进行排序,最后获取满足要求人的姓名。我们分别用传统方式和Stream的方式获取最终结果:
//初始化数据
List<People> peopleList = new ArrayList<>();
peopleList.add(new People("张三", 15));
peopleList.add(new People("李四", 41));
peopleList.add(new People("赵六", 35));
peopleList.add(new People("王五", 48));
peopleList.add(new People("啊大", 14));
peopleList.add(new People("啊二", 16));
----------------------------------------------------------------------------------------
//普通方式
List<People> peopleArrayList = new ArrayList<>();
//筛选出年龄小于40岁的人
for (People people : peopleList) {
if (people.getAge() < 40) {
peopleArrayList.add(people);
}
}
//将人按照年龄从小到大排序
peopleArrayList.sort(Comparator.comparing(People::getAge));
//提取人的名字
List<String> nameList = new ArrayList<>();
for (People people:peopleArrayList) {
nameList.add(people.getName());
}
List<People> peopleArrayList = new ArrayList<>();
//筛选出年龄小于40岁的人
for (People people : peopleList) {
if (people.getAge() < 40) {
peopleArrayList.add(people);
}
}
//将人按照年龄从小到大排序
peopleArrayList.sort(Comparator.comparing(People::getAge));
//提取人的名字
List<String> nameList = new ArrayList<>();
for (People people:peopleArrayList) {
nameList.add(people.getName());
}
System.out.println(nameList);
----------------------------------------------------------------------------------------
//Stream方法
List<String> nameList2 = peopleList.stream()
//过滤出年龄小于40的人
.filter(people1 -> people1.getAge() < 40)
//按照年龄排序
.sorted(Comparator.comparing(People::getAge))
//获取名称
.map(People::getName)
//返回结果
.collect(Collectors.toList());
System.out.println(nameList2);
可以看到,在普通方法中,我们多创建了一个peopleArrayList 垃圾变量,同时,在代码量上也多了很多。除此之外,如果我们想并行处理集合,只需要把people.stream改成people.parallelStream,流会自动帮你使用并行的方式处理数据,这在多核CPU上会提升处理效率。目前来看,流有几个显而易见的好处:
-
申明性:代码是以声明式方式写的:就像SQL语句一样,只是说明想完成什么,而不是说明如何实现。用这种方式加上行为参数化可以让我们很方便的应对需求
-
可复合:我们可以把几个基础操作链接起来,来表达复杂数据的处理流水线,同时保证代码清晰可读
-
可并行:我们可以简单的将集合并行处理,使性能更好。
流的简介
Stream相关类和接口在java.util.stream包中,我们可以通过很多方式获取到流,比如利用数值范围或者I/O资源生成流。流简短的定义就是“从支持数据处理操作的源生成的元素序列”。
- 元素序列:和集合一样,流提供了一个接口,可以访问特定元素的一组有序值。因为集合时数据结构,所以它的主要目的是以特定的时间/空间复杂度储存和访问数据。但流的目的是表达计算。因此,集合讲的是数据,流讲的是计算。
- 源:流会从源中获取要处理的流,有序集合生成流时会保持原来的顺序。
- 数据处理操作:对数据进行处理,包括过滤,排序,遍历等。流操作还可以顺序执行和并行执行。
- 流水线:很多流操作会返回处理后的流,这样我们就可以将多个操作链接起来。
- 内部迭代:流的迭代是在背后进行的,跟迭代器显式迭代不太一样。
流和集合的区别
什么时候进行读取和计算:
集合是一个内存中的数据结构,包含数据结构中目前所有的值,我们要先把所有的数据加载到集合中以后才可以对集合进行操作。就像我们我们用DVD看电影,包含电影的所有东西。要看必须要有完整的电影。流则是在概念上的固定的数据结构,其元素则是按需计算。例如我们在网上看电影,只需要加载目前进度的后面几分钟的电影就行。这样做有很大的好处,就是只有在我们需要的时候才会加载值。后面会有一个例子证明这点。
流只能遍历一次:
流和迭代器一样只能遍历一次,或者说只能消费一次。如果我们对一个流消费两次,那么代码会抛出java.lang.IllegalStateException:流已被操作或关闭。除非我们从数据源那再获取一个流。于此相反,我们可以对集合进行任意次的操作。
内部迭代和外部迭代:
我们使用集合或者迭代器的时候,需要我们自己去声明如何进行迭代,例如使用for循环或者是hasNext显性的去获取每一个元素,然后再在方法体中说明如何操作这些元素。这个就是外部迭代。对于流来说,你只需要申明对流进行什么操作。流会自动在内部对元素进行迭代。这就是内部迭代。
流的操作
java.util.stream.Stream中的Stream接口定义了很多操作。它们可以分成两大类:中间操作和终端操作。回顾一下上面的例子,讲述两类的区别:
List<String> nameList2 = peopleList
//获取流
.stream()
//中间操作
.filter(people1 -> people1.getAge() < 40)
//中间操作
.sorted(Comparator.comparing(People::getAge))
//中间操作
.map(People::getName)
//终端操作
.collect(Collectors.toList());
像filter、sorted、map等操作可以返回流,与其他操作连成一条流水线的流操作叫做中间操作。像collect这种会关闭流返回其他对象的操作叫做终端操作
中间操作
中间操作会返回一个新的流,例如从peopleList.stream()产生的流,在经过filter()以后,会产生一个新的流,给sorted()使用,在sorted()使用以后会产生一个流给map。这样不断的传递下去,就相当于工厂的流水线。还有很重要的一点,除非流水线触发一个终端操作,不然中间操作不会执行任何处理。因为中间操作能够合并起来,在终端操作时一次性全部处理。
List<String> nameList2 = peopleList.stream()
//过滤出年龄小于40的人
.filter(people1 ->{System.out.println("filter:"+people1.getAge());
return people1.getAge()<40;} )
//获取名称
.map(people -> {
System.out.println("map"+people.getName());
return people.getName();
})
//直取前三个
.limit(2)
//返回结果
.collect(Collectors.toList());
这个代码将会打印:
filter:15
map张三
filter:41
filter:35
map赵六
可以看出,流操作先将一个元素经过全部的中间操作以后,才会去执行另一个操作。这里利用了流的延迟性质。第一,尽管有很多人,但是我们只查询了前三个人就获得了我们要的结果。其次,我们将过滤和获取信息两个独立的操作合并到一次遍历中(可以更加清楚的看出我们的遍历过程,这个技术叫循环合并)。
终端操作
终端操作会从流水线中生成结果。返回结果可以是List,Integer,甚至是void。
使用流
一般来说,流的使用包括三个方面:
-
从数据源中创建一个流
-
对流进行中间操作
-
对流进行终端操作
下面两张图是常用的流操作:
流当然还有更多的操作,以及许多模式,比如切片、查找、匹配等,会在后面慢慢说。
更多与JDK1.8相关的文章请看:Java JDK1.8 核心特性详解----(总目录篇)