写在前面: Java8的Stream用起来真的不是一般的爽。当你看到Stream的操作后,相信你再也不会去写各种for循环、嵌套for循环,特别是做报表,体会更深.
What is Stream?
流(Stream)是Java API的新成员,它允许以声明性的方式处理数据集合(类似于数据库查询语句).暂且理解为遍历数据集的高级迭代器.
先举个尝尝鲜:
/*需求: 获取菜单中热量小于400卡路里的菜肴名称,并按照卡路里排序.*/@Data@Accessor(chain = true)public class Dish { // 该类将会在本文中多次用到 // omit getter,setter and constructor private String name; private boolean vegetarian; private int calories; private Type type; public enum Type {MEAT, FISH, OTHER}}// 普通写法public static ListgetLowCaloricDishesNamesInJava7(List dishes){ List lowCaloricDishes = new ArrayList<>(); for(Dish d: dishes){ if(d.getCalories() < 400){ lowCaloricDishes.add(d); } } List lowCaloricDishesName = new ArrayList<>(); Collections.sort(lowCaloricDishes, new Comparator () { public int compare(Dish d1, Dish d2){ return Integer.compare(d1.getCalories(), d2.getCalories()); } }); for(Dish d: lowCaloricDishes){ lowCaloricDishesName.add(d.getName()); } return lowCaloricDishesName;}// Stream 写法public static List getLowCaloricDishesNamesInJava8(List dishes){ return dishes.stream() .filter(d -> d.getCalories() < 400) .sorted(comparing(Dish::getCalories)) .map(Dish::getName) .collect(toList());}
从上面的代码可以明显看出区别,Stream写法更加简短、优美,并且可读性很强,我看到这段代码我就知道是干什么的.这就引申出Stream的优点:
- 声明性 -- 更简洁,更易读(表达能力很强)
- 可复合 -- 更灵活(可过滤,可映射,可排序...)
- 可并行 -- 性能更好(使用parallelStream)
Introduce of Stream
流就是从支持数据处理操作的源生成的元素序列
- 元素序列: 我理解的存储流数据的数据结构(?)
- 源: 提供数据的源,如集合,数组,输入/输出资源等.从有序集合生成流时会保留原有的顺序.
- 数据处理操作: filter,sort,map,find...流操作可以顺序执行,也可以并行执行.
Stream and Collection
集合是数据结构,所以它的主要目的是存储和访问集合元素,但流的目的是在于计算.
集合讲的是数据,集合可以遍历无数次,而流只能遍历一次,遍历完之后我们就说这个流被消费掉了.准确的说,流只能被消费一次,那些终端操作都是消费流.
Streams = Arrays.asList(1,2,3,4).stream();s.forEach(System.out.print); // 打印:1 2 3 4s.forEach(System.out.print); // 无打印
Inner Iteration and Outer Iteration
使用集合需要我们自己去做迭代(比如for-each),这就叫外部迭代.相反,Stream库使用内部迭代,也就是不需要我们去做迭代.比如:
// 还是上面那个Dish类,假设有个对象Listmenu, 要打印menu中所有菜肴的名称// 外部迭代for(Dish d : menu) { System.out.println(d.getName());}// 内部迭代menu.stream().map(Dish::getName).forEach(System.out::println);
Operations of Stream
先看一个例子:
menu.stream().filter(d -> d.getCalories() > 300).map(Dish::getName).forEach(System.out::println);
- 生成流: menu.steam(),生成流的方式有好多,如Arrays.stream(),可自己去查看Java API
- 中间操作(流的延迟性质): 处理流并返回流,比如filter,map,distinct等,类似fluent API.
- 终端操作: 消费流数据,比如forEach,collect等.
流的延迟性质
如果流水线上没有触发一个终端操作,那么中间操作是不会对流数据进行处理的.这是因为中间操作一般可以合并起来,在终端操作时一次性处理.比如:
Listnames = menu.stream().filter(d -> { System.out.println("filtering"); return d.getCalories() > 300; }).map(d -> { System.out.println("mapping"); return d.getName(); }).limit(3).collect(toList());/*上述的代码输出: filtering mapping filtering mapping filtering mapping*/
从上述的打印结果明显可以看出来,流会对中间操作进行合并,尽管filter和map是两个独立的操作,但它们合并到同一次遍历中了(循环合并).
Common Operations
很多流操作的方法参数类型都是函数式接口,这些函数式接口都是JDK自带的,本文将不会解释这些函数式接口,可以自己看接口的定义.
操作 | 类型 | 参数类型 | 函数描述符 | 描述 |
---|---|---|---|---|
filter | 中间 | Predicate<T> | T -> Boolean | 过滤 |
distinct | 中间 | 去重 | ||
skip | 中间 | long | 跳过前几项 | |
limit | 中间 | long | 只取前几项 | |
map | 中间 | Function<T,R> | T -> R | 映射 |
flatMap | 中间 | Function<T,Stream<R>> | T -> Stream<R> | 扁平化流 |
sorted | 中间 | Comparator<T> | (T,T) -> int | 排序 |
anyMatch | 终端 | Predicate<T> | T -> Boolean | 任意项匹配 |
noneMatch | 终端 | Predicate<T> | T -> Boolean | 无匹配 |
allMatch | 终端 | Predicate<T> | T -> Boolean | 所有匹配 |
findAny | 终端 | 返回任意项 | ||
findFirst | 终端 | 返回第一项 | ||
forEach | 终端 | Consumer<T> | T -> void | 遍历流 |
collect | 终端 | Collector<T,A,R> | 收集流数据 | |
reduce | 终端 | BinaryOperator<T> | (T,T) -> T | 归约 |
count | 终端 | long | 数量 |
上面这些都是常用的流操作,顺便提一下,使用skip和limit还可以做分页操作.下面讲解一下map,flatMap,reduce
映射: map
映射,也就是我从一个数据经过某些操作变成了另一个数据,也就是x --> y
x --f(x)--> y
举个栗子:
// 获取Listmenu中所有菜肴的名称Stream ds = menu.stream(); //菜单流Stream ns = menu.stream().map(e -> e.getName()); //菜肴名称流
扁平化: flatMap
扁平化流,这是<<Java8实战>>中这么翻译的,按我个人理解的话,我觉得flatMap就是合并流:
Stream+ Stream + Stream ==> Stream
举个栗子:
// 有一个String集合,需要将每个String切分成字符,并去重Listss = Arrays.asList("Hello", "World") .stream() // Stream .map(e -> e.split("")) // Stream .flatMap(Arrays::stream) // Stream .distinct() .collect(toList());ss.forEach(System.out::print);/*输出:Helowrd*/
对于上面这个栗子,执行map操作后返回Stream<String[]>(String[]指的是流元素的类型),接下来执行flatMap,将String[] -> Stream<String>, 然后将多个Stream<String>合并成一个Stream<String>.
下面这张图可以很形象地解释flatMap.归约: reduce
归约这个说法不太好理解,查看词典reduce还有个解释是"归纳为".wiki上对于的解释:
所谓的归约是将某个计算问题转换为另一个问题的过程。
也就是说reduce是描述如何从一个计算问题转换为另一个问题.大概是这么理解的吧.嗯,应该就是这样理解的(有木有大佬帮忙解释一下...〒︿〒).还是举几个栗子吧.
/*计算一个数值集合的总和*/Integer sum = Arrays.asList(4, 5, 3, 9).stream().reduce(0, (a, b) -> a + b);// 计算问题: 计算List的总和// 过程: reduce, 转换为另一个问题"可以设置一个初值为0, 然后每次累加, 也就是(a, b) -> a + b"// (可能理解有误)/*求一个数值集合的最大值*/Integer max = Arrays.asList(1, 2, 3, 4, 5).stream().reduce(Integer::max).orElse(0);
至于reduce方法具体是如何实现的,一步一步累加的过程,可以看下图:
reduce有三个重载方法,可根据需要使用对应的方法.
T reduce(T identity, BinaryOperatoraccumulator); Optional reduce(BinaryOperator accumulator); U reduce(U identity,BiFunction accumulator,BinaryOperator cobiner);
使用上面三个方法的时候要注意函数式接口的类型,以及泛型,下面我举个例子,计算菜单中所有的菜肴的热量总和,声明一点,下面的写法是非常不好的写法(shit code),只是单纯用来比较reduce三个重载方法的用法,以及写的时候要注意函数式接口的类型
Dish sum1 = dishes.stream().reduce(new Dish(), (a, b) -> new Dish().setCalories(a.getCalories() + b.getCalories()));System.out.println(sum1.getCalories());Dish sum2 = dishes.stream().reduce((a, b) -> new Dish().setCalories(a.getCalories() + b.getCalories())).get();System.out.println(sum2.getCalories());// 第三个参数暂时还不知道什么用处Integer sum3 = dishes.stream().reduce(0, (c, d) -> c + d.getCalories(), (a, b) -> a - b);System.out.println(sum3);
当然上面那个求和也可以这么写: Arrays.asList(4, 5, 3, 9).stream().mapToInt(e -> e).sum()
,其实sum的实现也是调的reduce方法.这里的mapToInt是转化成一个IntStream(数值流).
Numerical Stream
举个例子:
int calories = menu.stream().map(Dish::getCalories).reduce(0, Integer::sum);
这段代码的的问题是,它有一个暗含装箱的成本(为什么是装箱,而不是拆箱的成本?).
Java8引入了三个原始类型特化流接口来解决这个问题: IntStream,LongStream,DoubleStream,分别将流中的元素特化为int,long,double,从而避免了暗含的装箱成本.映射到数值流,可以使用mapToInt,mapToLong,mapToDouble,而转换为对象流直接调用boxed()方法即可.
Java8为数值流提供了很多的方法,比如sum,min,max,count,average等等.现在咱们就可以使用数值流来计算菜单中所有菜肴的热量总和:
int calories = menu.stream().mapToInt(Dish::getCalories).sum();
Summary
- 在Stream中使用了大量的函数式接口,结合来说的话,流是函数式接口的最佳实践,那么使用流是Lambda表达式的最佳实践
- Stream的专注点是处理数据,只能被消费一次(终端操作),而集合的重点是在于存取
- Stream的写法简短易读,一目了然
- Stream的常用操作,除了flatMap和reduce不太好理解,其他的都是顾名思义
- 数值流,减少了装箱的成本