本节重点内容
- 什么是流
- 集合与流
- 内部迭代与外部迭代
- 中间操作与终端操作
流是什么?
流是Java API的新成员,它允许你以声明式的方式处理数据集合(通过查询语句表达,而不是现写一个实现),可以把它看作遍历数据集的高级迭代器。
另外,流还可以透明的并行处理,无须写多余的多线程代码。
先看一段示例,以比较传统方法和流处理集合的不同:
// 现有一批菜肴,需要筛选出卡路里小于400且按卡路里排序,最后打印菜肴的名称
// java8之前的写法
public static void main(String[] args) {
List<Dish> menus = new ArrayList<>(List.of(
new Dish("大虾", 100),
new Dish("大鱼", 200),
new Dish("大兔", 300),
new Dish("大狗", 400),
new Dish("大猪", 500)
));
// java8前写法
List<String> dishNames = processOld(menus);
System.out.println(dishNames);
}
private static List<String> processOld(List<Dish> menu) {
List<Dish> lowCaloricDishes = new ArrayList<>();
// 先遍历筛选出低于400卡路里的菜
for (Dish d : menu) {
if (d.getCalories() < 400) {
lowCaloricDishes.add(d);
}
}
// 按卡路里排序
Collections.sort(lowCaloricDishes, new Comparator<>() {
public int compare(Dish d1, Dish d2) {
return Integer.compare(d1.getCalories(), d2.getCalories());
}
});
// 最后收集菜肴名称
List<String> lowCaloricDishesName = new ArrayList<>();
for (Dish dish : lowCaloricDishes) {
lowCaloricDishesName.add(dish.getName());
}
return lowCaloricDishesName;
}
java8使用流的写法
public static void main(String[] args) {
List<Dish> menus = new ArrayList<>(List.of(
new Dish("大虾", 100),
new Dish("大鱼", 200),
new Dish("大兔", 300),
new Dish("大狗", 400),
new Dish("大猪", 500)
));
// stream写法
List<String> dishNames = menus.stream().filter(e -> e.getCalories() < 400).sorted(comparing(Dish::getCalories)).map(Dish::getName).collect(Collectors.toList());
System.out.println(dishNames);
}
采用stream处理的好处
- 代码以声明式的方式写的:说明想要完成什么,而不是说明如何实现(通过for, if等)
- 可以把几个基础操作链接起来,来表达复杂的数据处理流水线,同时保持代码清晰可读。
前人的尝试
在没有StreamAPI之前,很多厂商和个人为了解决繁琐的集合操作,已经做了诸多努力。比如Guava就是Google创建的一个非常流行的库。久远一点的Apache Commons Collections库也提供类似的功能。
如今通过Java8的StreamAPI可写出声明式,可复合,可并行的代码。
流的简介
流的定义:从支持数据处理操作的源生成的元素序列
元素序列:可以访问特定元素类型的一组有序值
源:流会使用一个提供数据的源,如:集合,数组,输入/输出资源。注意:从有序集合生成的流会保持原有的顺序
数据处理操作:流支持的操作类似于数据库的操作,以及函数式编程语言中的常用操作,如:filter, map, reduce, find, match, sort等。流操作可以顺序执行,也可以并行执行。
流水线:很多流操作本身会返回一个流,这样可以链式操作形成一个大的流水线。
内部迭代:自己写for-each为显式迭代,自行定义具体步骤,而流的迭代是在背后进行的,所以称为内部迭代。
以下仍以菜肴为示例进行说明:
menus.stream() // 从集合生成流
.filter(e -> e.getCalories() < 400)
.sorted(comparing(Dish::getCalories))
.map(Dish::getName)
.limit(2)
.collect(Collectors.toList());
// filter, sorted, map, limit都是流操作,本身又返回另一个流,形成一个流水线
// collect处理流水线,将结果返回,本例中返回一个集合
// 注意:在调用collect前,没有任何结果产生,根本未实际执行,都在排队等待,直到collect的到来。
流与集合
集合和流的区别:
它们的差异在于什么时候进行计算,集合是内存中的数据结构,它包含数据结构中目前所有的值-集合中的每个元素都是先计算出来才能添加到集合中。
而流元素则是按需计算的,像是一个延迟创建的集合,只有在有需要时才创建值。
只能遍历一次
和迭代器类似,流只能遍历一次。一次结束了这个流就被消费了,可以从原始源重新获取一个流(前提是可重复的源,如果IO通道就不行了)。
记住:流只能被消费一次
public class DishStreamOnceSample {
public static void main(String[] args) {
List<Dish> menus = new ArrayList<>(List.of(
new Dish("大虾", 100),
new Dish("大鱼", 200),
new Dish("大兔", 300),
new Dish("大狗", 400),
new Dish("大猪", 500)
));
// 演示流只能消费一次
Stream<Dish> stream = menus.stream();
stream.forEach(System.out::println);
stream.forEach(System.out::println);
}
}
// result:
Dish(name=大虾, Calories=100)
Dish(name=大鱼, Calories=200)
Dish(name=大兔, Calories=300)
Dish(name=大狗, Calories=400)
Dish(name=大猪, Calories=500)
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
at java.base/java.util.stream.AbstractPipeline.sourceStageSpliterator(AbstractPipeline.java:279)
at java.base/java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:658)
at win.elegentjs.java8.stream.DishStreamOnceSample.main(DishStreamOnceSample.java:28)
Process finished with exit code 1
外部迭代&内部迭代
用户自行使用for-each,这称为外部迭代。而内部迭代是指流内部执行。
内部迭代可以透明的并行处理,或者用更优化的顺序处理。
流操作
java.util.stream.Stream中的Stream接口定义了许多操作。它们可以分为两大类:
1)中间操作:filter, map,limit等
2)终端操作:collect,用来触发流行线执行并关闭流水线
中间操作
如filter,sorted等中间操作会返回另一个流,可通过链式操作形成一个流水线,但重要的是中间操作不会触发流的执行。StreamAPI会自动优化相关步骤,如自动合并,短路。
示例如下:
public class DishStreamDebugSample {
public static void main(String[] args) {
List<Dish> menus = new ArrayList<>(List.of(
new Dish("大虾", 100),
new Dish("大鱼", 200),
new Dish("大兔", 300),
new Dish("大狗", 400),
new Dish("大猪", 500)
));
List<String> dishNames = menus.stream().
filter(e -> {
System.out.println(e.getCalories() + " filter");
return e.getCalories() < 600;
})
.map(e -> {
System.out.println(e.getCalories() + " map");
return e.getName();
}).limit(2)
.collect(Collectors.toList());
System.out.println(dishNames);
}
}
// result:
// 100 filter
// 100 map
// 200 filter
// 200 map
// [大虾, 大鱼]
可以看出并非是先过滤所有的集合,且filter和map做了合并,limit实现了短路效果。
终端操作
终端操作从流水线生成结果,其结果可以是不是流的任何值,如List, Integer,甚至void。如:
menus.stream().forEach(System.out::println);
此处的forEach是一个终端操作,它的返回值是void。
使用流
流的使用包括三件事:
1)一个数据源
2)一个中间操作链,形成一条流水线
3)一个终端操作,执行流水线,并生成结果
列举几个中间操作和终端操作(部分,不全)
小结
本节初步学习了什么是流,了解了以下重要概念:
1)流是从支持数据处理操作的源生成的一组元素
2)流利用内部迭代
3)流操作有两类:中间操作和终端操作
4)多个中间操作可串联出流水线,但不会生成任何结果
5)forEach和count等终端操作触发流水线执行,并返回一个非流的值
6)流中的元素计算按需进行