引言
本篇博客主要基于<Java 8 in Action>、<Java 8函数式编程>等书总结一下Stream流概况。
一:流是什么?
首先来看一下声明性方式处理数据集合带来的差异。
首先我们一个实体类型Dish
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@AllArgsConstructor
@Setter
@Getter
@ToString
public class Dish {
private String name;
private boolean vegetarian;
private int calories;
private Type type;
public enum Type {MEAT, FISH, OTHER}
}
然后我们试着mock一个Dish集合,menu。我们要进行的操作是将menu中所有calories小于400的Dish name有序集合返回。
集合处理
// java 7 in Action
List<Dish> menu = Lists.newArrayList();
List<Dish> lowCaloricDishes = Lists.newArrayList();
for (Dish d : menu) {
if (d.getCalories() < 400) {
lowCaloricDishes.add(d);
}
}
Collections.sort(lowCaloricDishes, new Comparator<Dish>() {
@Override
public int compare(Dish o1, Dish o2) {
return Integer.compare(o1.getCalories(), o2.getCalories());
}
});
List<String> lowCaloricDishesName = Lists.newArrayList();
for (Dish d : lowCaloricDishes) {
lowCaloricDishesName.add(d.getName());
}
流处理
// java 8 in Action
menu.stream()
.filter(d -> d.getCalories() < 400)
.sorted(Comparator.comparing(Dish::getCalories))
.map(Dish::getName)
.collect(Collectors.toList());
并行流处理
// java 8 in action
menu.parallelStream()
.filter(d -> d.getCalories() < 400)
.sorted(Comparator.comparing(Dish::getCalories))
.map(Dish::getName)
.collect(Collectors.toList());
总结一下以上代码。
Java 7 in Action
- 代码说明如何实现
- 有"垃圾变量"lowCaloricDishes
- 需要仔细阅读实现才能知道流水线做了哪些事情
- 如果要并行处理,需要自己去实现多线程代码
- 并行处理中需要自己设计与保证线程安全
- 显示迭代,需要自己优化迭代性能
Java 8 in Action
- 声明性代码,表达目的,而不是实现
- 整个流水线可读性强
- 并行操作无需自己处理
- 代码简单
- 可享受隐式迭代jdk平滑升级的性能优化
二:流简介
简短定义
"从支持数据处理操作的源生成的元素序列"
- 元素序列
就像集合一样,流也提供了一个接口,可以访问特定元素类型的一组有序值。因为集合是数据结构,所以它的主要目的是以特定的时间/空间复杂度存储和访问元素(如ArrayList与LinkedList)。但流的目的在于表达计算,比如前面的filter、sorted、map。集合讲的是数据,流讲的是计算。 - 源
流会使用一个提供数据的源,如集合、数组、输入/输出资源。请注意,从有序集合生成的流会保留原有的顺序。 - 数据处理操作
类似于数据库的操作,以及函数式编程语言中的操作。如filter、map、reduce、find、match、sort等。流操作可以顺序执行,也可以并行执行。 - 流水线
很多流操作本身会返回一个流,这样多个操作就可以链接起来,形成一个大的流水线。流水线的操作可以看作对数据源进行数据库式查询。 - 内部迭代
与显示迭代不同,流的迭代操作是在背后进行的
三:流与集合
粗略的说:"流和集合之间的差异在于什么时候进行计算,还有遍历的方式"
- 流只能遍历一次
如果对同一个流使用两次,将会出现此类异常。
Stream<Dish> stream = menu.stream();
stream.collect(Collectors.toList());
stream.collect(Collectors.toList());
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:229)
at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499)
at com.zhengyu.stream.StreamAction.main(StreamAction.java:39)
-
计算时机不同
流可以延迟懒加载、按需使用、实时计算
集合必须准备全部数据 -
遍历方式
集合属于显示遍历,需要自己处理优化&线程安全&并行
流属于隐式遍历,Java 8需要一个类似于Collection却没有迭代器的接口,于是就有了Stream。
四:流操作
流的使用一般包括三件事
- 一个数据源来执行一个查询
- 一个中间操作链,形成一条流的流水线
- 一个终端操作,执行流水线,并能生成结果
整个过程类似于builder模式,在builder模式中有一个调用链用来设置一套配置(相当于中间操作),最后调用一个build方法生成结果(相当于终端操作)。
// java 8 in action
menu.stream()
.filter(d -> d.getCalories() < 400)
.sorted(Comparator.comparing(Dish::getCalories))
.map(Dish::getName)
.collect(Collectors.toList());
可以看到两类操作:
- 中间操作
可以链接起来的操作成为"中间操作",如filter、sorted、map、limit。 - 终端操作
关闭流的操作成为"终端操作",如collect、find、count。
在中间操作中,还会有短路技巧与循环合并技术,能够按需计算,减少遍历的次数。
// java 8 in action
menu.stream()
.filter(d -> {
System.out.println("filtering" + d.getName());
return d.getCalories() < 400;
})
.sorted(Comparator.comparing(Dish::getCalories))
.map(d -> {
System.out.println("mapping" + d.getName());
return d.getName();
})
.limit(3)
.collect(Collectors.toList());
如上述代码,由于limit和短路,加上filter和map循环合并。
最终只会打印三条filtering和mapping日志。
image.png









网友评论