Java Stream
最近看到一篇讲stream的博文,想着自己平时中也不少用到流的操作。于是就结合着博文和自己的应用来做一个整理。以备以后自己查字典用。
1. Stream的优点
Stream是JDK1.8引入的新特性。它能让我们通过lambda表达式更简明扼要的以流水线的方式去处理集合内的数据,可以很轻松的完成诸如:过滤、分组、收集、归约这类操作。
1.1 代码结构更加简洁清晰
举个例子。这里有一组整数,要取大于2小于10,且为偶数的list。如果不使用Stream,那么代码如下:
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 8);
List<Integer> filter2 = new ArrayList<>();
for (Integer i : list) {
if (i > 2 && i < 10 && (i % 2 == 0)) {
filter2.add(i);
}
}
System.out.println(filter2);
如果后续条件增加,那么if中会有大量的判断,导致代码不断冗余,可读性越来越差。
如果换成stream,那么代码是这样的:
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 8).stream()
.filter(i -> i > 2)
.filter(i -> i < 10)
.filter(i -> i % 2 == 0)
.collect(Collectors.toList());
System.out.println(list);
可以发现,用了stream以后,只需要关注筛选条件就够了。filter这个方法名能让你清楚的知道它是个过滤条件,collect这个方法名也能看出来它是一个收集器,将最终结果收集到一个List里面去。
1.2 不必关心变量状态
Stream在设计的时候就被设计为不可变的。它的不可变有两种含义:
- 由于每次Stream操作都会生成一个新的Stream,所以Stream是不可变的,就像String。
- 在Stream中只保存原集合的引用,所以在进行一些会修改元素的操作时,是通过原元素生成一份新的新元素,所以Stream 的任何操作都不会影响到原对象。
第一个含义可以帮助我们进行链式调用,实际上我们使用Stream的过程中往往会使用链式调用,而第二个含义则是函数式编程中的一大特点:不修改状态。
无论对Stream做怎么样的操作,它最终都不会影响到原集合,它的返回值也是在原集合的基础上进行计算得来的。 所以在Stream中我们不必关心操作原对象集合带来的种种副作用,用就完了。
1.3 延迟执行与优化
Stream只在遇到终结操作
的时候才会执行,比如:
Arrays.asList(1, 2, 3, 4, 5, 6, 8).stream()
.filter(i -> i > 2)
.peek(System.out::println);
这么一段代码是不会执行的,peek方法可以看作是forEach,这里我用它来打印Stream中的元素。因为peek方法和filter方法都是流转换方法,所以不会触发执行。
如果在后面加入一个count方法就能正常执行。count是终结操作,stream这种只有终结操作才能触发执行的特性被叫做延迟执行
。
2. 创建Stream
2.1 通过Stream接口创建
Stream.of()
Stream<Integer> integerStream = Stream.of(1, 2, 3);
Stream<Double> doubleStream = Stream.of(1.1d, 2.2d, 3.3d);
Stream<String> stringStream = Stream.of("1", "2", "3");
Stream.empty()
Stream<Object> empty = Stream.empty();
Stream.generate()
以上都是我们让我们易于理解的创建方式,还有一种方式可以创建一个无限制元素数量的
Stream——generate():
从方法参数上来看,它接受一个函数式接口——Supplier作为参数,这个函数式接口是用来创建对象的接口,你可以将其类比为对象的创建工厂,Stream将从此工厂中创建的对象放入Stream中:
Stream<Integer> generateInteger = Stream.generate(() -> 12321);
Stream<String> generate = Stream.generate(() -> "Supplier1");
这里是为了方便直接使用Lamdba构造了一个Supplier对象,你也可以直接传入一个Supplier对象,它会通过Supplier接口的get() 方法来构造对象:
2.2 通过集合类库进行创建
相较于上面的Stream创建来说,第二种方式更较为常用。直接通过集合进行Stream的流操作。
Stream<String> listStream = Arrays.asList("qaz", "wsx", "edc", "rfv").stream();
Set<String> sets = new TreeSet<>();
Stream<String> setStream = sets.stream();
通过源码,可以发现 stream()
方法本质上还是通过调用一个Stream工具类来创建。
2.3 创建并行流
Stream.of().parallel()
Stream<Integer> intParallelStream = Stream.of(1, 2, 3).parallel();
Stream<String> strParallelStream = Stream.of("1", "2", "3").parallel();
List.parallelStream()
List<String> strList = Arrays.asList("qaz", "wsx", "edc", "rfv");
Stream<String> strParallelStreamList = strList.parallelStream();
List<Integer> intList = Arrays.asList(1, 2, 3, 4, 5, 6, 8);
Stream<Integer> intParallelStreamList = intList.parallelStream();
从集合的stream和parallelStream的构造方法的源码来看,只有参数的区别。
一般情况下并不需要用到并行流,在Stream中元素不过千的情况下性能并不会有太大提升。 并行的好处是充分利用多核CPU的性能,但是使用中往往要对数据进行分割,然后分散到各个CPU上去处理,如果我们使用的数据是数组结构则可以很轻易的进行分割,但是如果是链表结构的数据或者Hash结构的数据则分割起来很明显不如数组结构方便。
所以只有当Stream中元素过万甚至更大时,选用并行流才能带给你更明显的性能提升。
Stream.of().parallel().sequential()
并行流转换成串行流
Stream<String> strStream = Stream.of("1", "2", "3").parallel().sequential();
2.4 连接Stream
相同泛型concat
Stream<Integer> concat = Stream
.concat(Stream.of(1, 2, 3), Stream.of(4, 5, 6));
concat.peek(System.out::println)
.count();
不同泛型concat
Stream<? extends Serializable> concat = Stream
.concat(Stream.of(1, 2, 3), Stream.of("qaz", "wsx", "edc"));
concat.peek(System.out::println)
.count();
2.5 转换操作
2.5.1无状态方法
方法的执行无需依赖前面方法执行的结果集
- map()方法:此方法的参数是一个Function对象,它可以使你对集合中的元素做自定义操作,并保留操作后的元素。
- filter()方法:此方法的参数是一个Predicate对象,Predicate的执行结果是一个Boolean类型,所以此方法只保留返回值为true的元素,正如其名我们可以使用此方法做一些筛选操作。
- flatMap()方法:此方法和map()方法一样参数是一个Function对象,但是此Function的返回值要求是一个Stream,该方法可以将多个Stream中的元素聚合在一起进行返回。
map()
例子:
List<Integer> intList = Arrays.asList(1, 2, 3, 4, 5, 6, 8);
intList.stream().map(i -> i * 10).peek(System.out::println)
.count();
一个list,对其中每个元素扩大10倍。
filter()
例子:
List<Integer> intList = Arrays.asList(1, 2, 3, 4, 5, 6, 8);
intList.stream().filter(i -> i >= 2).peek(System.out::println)
.count();
返回list中,大于等于2的元素。
flatMap()
根据官方文档的说法,此方法是为了进行一对多元素的平展操作:
List<Order> orderList = Arrays.asList(new Order(), new Order(), new Order());
Stream<Item> itemStream = orderList.stream().flatMap(order ->
order.getItemList().stream());
这里通过一个订单示例来说明此方法,我们的每个订单中都包含了一个商品List,如果我想要将两个订单中所有商品List组成一个新的商品List,就需要用到flatMap()方法。
在上面的代码示例中可以看到每个订单都返回了一个商品List的Stream,在本例中只有三个订单,所以也就是最终会返回三个商品List的Stream,flatMap()方法的作用就是将这三个Stream中元素提取出来然后放到一个新的Stream中。
mapToInt
mapToLong
mapToDouble
flatMapToInt
flatMapToLong
flatMapToDouble
这六个方法首先从方法名中就可以看出来,它们只是在map()或者flatMap()的基础上对返回值进行转换操作。
- IntStream:对应基础数据类型中的 int、short、char、boolean
- LongStream:对应基础数据类型中的 long
- DoubleStream:对应基础数据类型中的 double和float
peek()
可以通过peek方法做些打印元素之类的操作:
List<Integer> intList = Arrays.asList(1, 2, 3, 4, 5, 6, 8);
intList.stream().filter(i -> i >= 2).peek(System.out::println)
.count();
如果不太熟悉的话,不建议使用,某些情况下它并不会生效,比如:
List<Integer> intList = Arrays.asList(1, 2, 3, 4, 5, 6, 8);
intList.stream()
.map(i -> i * 10)
.peek(System.out::println)
.count();
API文档上面也注明了此方法是用于Debug,【通过我的经验】只有当Stream最终需要重新生产元素时,peek才会执行。上面的例子中,count只需要返回元素个数,所以peek没有执行,如果换成collect方法就会执行。或者如果Stream中存在过滤方法如filter方法和match相关方法,它也会执行。
无状态方法的循环合并
上文中的一个例子:
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 8).stream()
.filter(i -> i > 2)
.filter(i -> i < 10)
.filter(i -> i % 2 == 0)
.collect(Collectors.toList());
System.out.println(list);
其中调用了三次filter方法,stream循环会进行几次过滤?
如果把filter换为map,stream循环又是几次?
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 8).stream()
.map(i -> i * 10)
.filter(i -> i < 10)
.filter(i -> i % 2 == 0)
.collect(Collectors.toList());
System.out.println(list);
从直觉来看,需要执行三次循环。但filter,map是无状态方法,这三个条件可以放在一个循环里实现,原因是filter只依赖map的计算结果,而不依赖map执行完的结果集,所以只要保证先操作map再操作filter,它们就可以在一次循环里完成,这种优化方式叫做循环合并
。
所有的无状态方法都可以放在同一个循环内执行,它们也可以方便的使用并行流在多个CPU上执行。
2.5.2有状态方法
方法的执行依赖前面方法执行的结果集
方法 | 结果 |
---|---|
distinct() | 元素去重 |
sorted() | 元素排序,重载的两个方法,需要的时候可以传入一个排序对象 |
limit(long maxSize) | 传入一个数字,代表只取前X个元素 |
skip(long n) | 传入一个数字,代表跳过X个元素,取后面的元素 |
takeWhile(Predicate predicate) | JDK9新增,传入一个断言参数当第一次断言为false时停止,返回前面断言为true的元素 |
dropWhile(Predicate predicate) | JDK9新增,传入一个断言参数当第一次断言为false时停止,删除前面断言为true的元素 |
以上就是所有的有状态方法,它们的方法执行都必须依赖前面方法执行的结果集才能执行,比如排序方法就需要依赖前面方法的结果集才能进行排序。
同时limit方法和takeWhile是两个短路操作方法,这意味效率更高,因为可能内部循环还没有走完时就已经选出了我们想要的元素。
所以有状态的方法不像无状态方法那样可以在一个循环内执行,每个有状态方法都要经历一个单独的内部循环,所以编写代码时的顺序会影响到程序的执行结果以及性能,望注意。
2.6 终结操作
聚合方法特性
- 聚合方法代表着整个流计算的最终结果,所以它的返回值都不是Stream。
- 聚合方法返回值可能为空,比如filter没有匹配到的情况,JDK8中用Optional来规避NPE(NullPointerException)。
- 聚合方法都会调用evaluate方法,这是一个内部方法,看源码的过程中可以用它来判定一个方法是不是聚合方法。
2.6.1 简单聚合
count()
:返回Stream中元素的size大小。forEach()
:通过内部循环Stream中的所有元素,对每一个元素进行消费,此方法没有返回值。forEachOrder()
:和上面方法的效果一样,但是这个可以保持消费顺序,哪怕是在多线程环境下。anyMatch(Predicate predicate)
:这是一个短路操作,通过传入断言参数判断是否有元素能够匹配上断言。allMatch(Predicate predicate)
:这是一个短路操作,通过传入断言参数返回是否所有元素都能匹配上断言。noneMatch(Predicate predicate)
:这是一个短路操作,通过传入断言参数判断是否所有元素都无法匹配上断言,如果是则返回true,反之则false。findFirst()
:这是一个短路操作,返回Stream中的第一个元素,Stream可能为空所以返回值用Optional处理。findAny()
:这是一个短路操作,返回Stream中的任意一个元素,串型流中一般是第一个元素,Stream可能为空所以返回值用Optional处理。
这里面着重说下短路操作方法:
findFirst()
和findAny()
这两个方法, 由于它们只需要拿到一个元素就能方法就能结束,所以短路效果很好理解。
anyMatch
方法,它只需要匹配到一个元素方法也能结束,所以它的短路效果也很好理解。
allMatch
方法和noneMatch
,乍一看这两个方法都是需要遍历整个流中的所有元素的,其实不然,比如allMatch只要有一个元素不匹配断言它就可以返回false了,noneMatch只要有一个元素匹配上断言它也可以返回false了,所以它们都是具有短路效果的方法。
2.6.2 归约
reduce:反复求值
这里解释下规约的含义:将一个Stream中的所有元素反复结合起来,得到一个结果,这样的操作被称为归约
。
举个简单的例子,1、2、3三个元素,将它们两两相加,最后得出6这个数字,这个过程就是归约。再比如,还是1、2、3三个元素,将它们两两比较,最后挑出最大的数字3或者挑出最小的数字1,这个过程也是归约。
归约是两两元素进行处理然后得到一个最终值,所以reduce的方法的参数是一个二元表达式,它将两个参数进行任意处理,最后得到一个结果,其中它的参数和结果必须是同一类型。
List<Integer> intList = Arrays.asList(1, 2, 3, 4, 5, 6, 8);
Optional<Integer> reduce = intList.stream()
.reduce((i1, i2) -> i1 + i2);
System.out.println(reduce.get());
比如代码中的,i1和i2就是二元表达式的两个参数,它们分别代表元素中的第一个元素和第二个元素,当第一次相加完成后,所得的结果会赋值到i1身上,i2则会继续代表下一个元素,直至元素耗尽,得到最终结果。
这里还有一个优雅的写法
List<Integer> intList = Arrays.asList(1, 2, 3, 4, 5, 6, 8);
Optional<Integer> reduce = intList.stream()
.reduce(Integer::sum);
System.out.println(reduce.get());
这是一个以
方法引用
代表lambda表达式的例子。
reduce的返回值是Optional的,这是预防Stream没有元素的情况。 你也可以想办法去掉这种情况,那就是让元素中至少要有一个值,这里reduce提供一个重载方法给我们:
List<Integer> intList = Arrays.asList(1, 2, 3, 4, 5, 6, 8);
Integer reduce = intList.stream()
.reduce(0, (i1, i2) -> i1 + i2);
System.out.println(reduce);
max:利用归约求最大
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 8);
Optional<Integer> max = list.stream().max(Integer::compare);
System.out.println(max.get());
可以发现,max方法是需要传参的,还有一种方法无需传参也可以拿到最大值:
OptionalInt max = IntStream.of(1, 2, 3, 4, 5, 6, 8).max();
System.out.println(max.getAsInt());
OptionalInt
是Optional
对基础类型int的封装。
min:利用归约求最小
例子:
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 8);
Optional<Integer> min = list.stream().min(Integer::compare);
System.out.println(min.get());
2.6.3 收集器
收集器collect方法:
收集器是用来收集Stream的元素的,最后收集成什么可以自定义,但是一般不需要自己写,因为JDK内置了一个Collector的实现类——Collectors。
收集方法
通过Collectors,可以利用它的内置方法很方便的进行数据收集:
比如想把元素收集成集合,那么可以使用toCollection
或者toList
方法,不过我们一般不使用toCollection
,因为它需要传参。
也可以使用toUnmodifiableList
(因为我用的是JDK8,所以没有此类方法),它和toList区别就是它返回的集合不可以改变元素,比如删除或者新增。
再比如要把元素去重之后收集起来,那么你可以使用toSet
。
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 8);
List<Integer> toList = list.stream().collect(Collectors.toList());
Set<Integer> toSet = list.stream().collect(Collectors.toSet());
可以看到,toList
底层是经典的ArrayList
,toSet
底层是经典的HashSet
。
有时候根据业务需求也需要收集成一个Map,那么可以使用toMap()
:
List<Order> list = Arrays.asList(new Order(), new Order(), new Order(), new Order());
Map<Integer, List<Item>> toMap = list.stream().
collect(Collectors.toMap(Order::getOrderNo, itm -> itm.getList()));
toMap()
具有两个参数:
- 第一个参数代表key,它表示你要设置一个Map的key,这里指定的是元素中的orderNo。
- 第二个参数代表value,它表示你要设置一个Map的value,这里直接把元素的list当作值,所以结果是一个Map<Integer, List<Item>>。
toMap() 有两个伴生方法:
- toUnmodifiableMap():返回一个不可修改的Map。
- toConcurrentMap():返回一个线程安全的Map。
toMap() 也有一个缺点。就是HashMap相同的key会进行覆盖操作。toMap() 方法生成Map时,如果key出现了重复,那么它会直接抛出异常。
分组方法
如果要对数据进行分类,而key是可以重复的,那么可以使用groupingBy
。
例子:
List<Item> list1 = new ArrayList<>();
list1.add(new Item(4));
list1.add(new Item(5));
list1.add(new Item(6));
List<Item> list2 = new ArrayList<>();
list2.add(new Item(1));
list2.add(new Item(2));
list2.add(new Item(3));
List<Order> orders = Arrays.asList(
new Order(1, "name1", list1),
new Order(1, "name1", list2),
new Order(2, "name2", list1));
Map<Long, List<Order>> collect = orders.stream()
.collect(Collectors.groupingBy(Order::getOrderNo));
System.out.println(collect);
Map<String, List<Order>> collect1 = orders.stream()
.collect(Collectors.groupingBy(Order::getName));
System.out.println(collect1);
结果:
{
1=[
Order(orderNo=1, name=name1, itemList=[Item(id=4), Item(id=5), Item(id=6)]),
Order(orderNo=1, name=name1, itemList=[Item(id=1), Item(id=2), Item(id=3)])
],
2=[
Order(orderNo=2, name=name2, itemList=[Item(id=4), Item(id=5), Item(id=6)])
]
}
{
name2=[
Order(orderNo=2, name=name2, itemList=[Item(id=4), Item(id=5), Item(id=6)])
],
name1=[
Order(orderNo=1, name=name1, itemList=[Item(id=4), Item(id=5), Item(id=6)]),
Order(orderNo=1, name=name1, itemList=[Item(id=1), Item(id=2), Item(id=3)])
]
}
groupingBy还有个重载方法,可以自定义收集器类型,它的第二个参数是一个Collector收集器对象。
Map<String, Set<Order>> collect2 = orders.stream()
.collect(Collectors.groupingBy(Order::getName,toSet()));
System.out.println(collect2);
注:对于Collector类型,通常使用Collectors类,这里由于前面用了Collectors,所以这里不必声明直接传入一个toSet()方法,代表将分组后的元素收集为Set。
分区方法
partitioningBy
List<Item> list1 = new ArrayList<>();
list1.add(new Item(21));
list1.add(new Item(54));
list1.add(new Item(76));
List<Item> list2 = new ArrayList<>();
list2.add(new Item(34));
list2.add(new Item(19));
list2.add(new Item(83));
List<Order> orders = Arrays.asList(
new Order(15, "name51", list1, 1),
new Order(15, "name51", list2, 0),
new Order(25, "name52", list1, 1));
Map<Boolean, List<Order>> rest = orders.stream()
.collect(Collectors.partitioningBy(i -> 1 == i.getPaid()));
System.out.println(rest);
Map<Boolean, Set<Order>> rest1 = orders.stream()
.collect(Collectors.partitioningBy(ord -> 0 == ord.getPaid(), toSet()));
System.out.println(rest1);
结果:
{
false=[
Order(orderNo=15, name=name51, itemList=[Item(id=34), Item(id=19), Item(id=83)], paid=0)
],
true=[
Order(orderNo=15, name=name51, itemList=[Item(id=21), Item(id=54), Item(id=76)], paid=1),
Order(orderNo=25, name=name52, itemList=[Item(id=21), Item(id=54), Item(id=76)], paid=1)
]
}
{
false=[
Order(orderNo=25, name=name52, itemList=[Item(id=21), Item(id=54), Item(id=76)], paid=1),
Order(orderNo=15, name=name51, itemList=[Item(id=21), Item(id=54), Item(id=76)], paid=1)
],
true=[
Order(orderNo=15, name=name51, itemList=[Item(id=34), Item(id=19), Item(id=83)], paid=0)
]
}
partitioningBy() 重载
Collectors中的Stream方法
- map → mapping
- filter → filtering
- flatMap → flatMapping
- count → counting
- reduce → reducing
- max → maxBy
- min → minBy
counting()方法
List<Item> list1 = new ArrayList<>();
list1.add(new Item(23));
list1.add(new Item(14));
list1.add(new Item(64));
List<Item> list2 = new ArrayList<>();
list2.add(new Item(62));
list2.add(new Item(35));
list2.add(new Item(75));
List<Order> orders = Arrays.asList(
new Order(26, "name64", list1, 1),
new Order(15, "name51", list2, 0),
new Order(25, "name52", list1, 1));
Map<String, Long> collect = orders.stream()
.collect(Collectors.groupingBy(Order::getName, counting()));
System.out.println(collect);
结果:
{name64=1, name52=1, name51=1}
minBy()方法
List<Item> list1 = new ArrayList<>();
list1.add(new Item(35));
list1.add(new Item(13));
list1.add(new Item(53));
List<Item> list2 = new ArrayList<>();
list2.add(new Item(51));
list2.add(new Item(12));
list2.add(new Item(76));
List<Order> orders = Arrays.asList(
new Order(26, "name64", list1, 1, 22),
new Order(26, "name64", list1, 1, 34),
new Order(15, "name51", list2, 0, 93),
new Order(15, "name51", list2, 0, 21),
new Order(25, "name52", list1, 1, 63),
new Order(25, "name52", list1, 1, 29));
Map<String, Optional<Order>> collect = orders.stream()
.collect(Collectors.groupingBy(Order::getName, minBy(Comparator.comparing(Order::getMoney))));
System.out.println(collect);
结果:
{
name64=Optional[
Order(orderNo=26, name=name64, itemList=[Item(id=35), Item(id=13), Item(id=53)], paid=1, money=22)
],
name52=Optional[
Order(orderNo=25, name=name52, itemList=[Item(id=35), Item(id=13), Item(id=53)], paid=1, money=29)
],
name51=Optional[
Order(orderNo=15, name=name51, itemList=[Item(id=51), Item(id=12), Item(id=76)], paid=0, money=21)
]
}
summingLong()方法
List<Item> list1 = new ArrayList<>();
list1.add(new Item(12));
list1.add(new Item(41));
list1.add(new Item(123));
List<Item> list2 = new ArrayList<>();
list2.add(new Item(54));
list2.add(new Item(14));
list2.add(new Item(63));
List<Order> orders = Arrays.asList(
new Order(26, "name64", list1, 1, 22),
new Order(26, "name64", list1, 1, 34),
new Order(15, "name51", list2, 0, 93),
new Order(15, "name51", list2, 0, 21),
new Order(25, "name52", list1, 1, 63),
new Order(25, "name52", list1, 1, 29));
Map<String, Long> collect = orders.stream()
.collect(Collectors.groupingBy(Order::getName, summingLong(Order::getMoney)));
System.out.println(collect);
结果:
{name64=56, name52=92, name51=114}
类似的还有 summingInt
,summingDouble
,averagingLong
,averagingDouble
,averagingInt
joining()方法
List<Item> list1 = new ArrayList<>();
list1.add(new Item(42));
list1.add(new Item(11));
list1.add(new Item(83));
List<Item> list2 = new ArrayList<>();
list2.add(new Item(44));
list2.add(new Item(90));
list2.add(new Item(20));
List<Order> orders = Arrays.asList(
new Order(26, "name64", list1, 1, 22),
new Order(26, "name64", list1, 1, 34),
new Order(15, "name51", list2, 0, 93),
new Order(15, "name51", list2, 0, 21),
new Order(25, "name52", list1, 1, 63),
new Order(25, "name52", list1, 1, 29));
String collect = orders.stream()
.map(Order::getName).collect(Collectors.joining(","));
System.out.println(collect);
结果:
name64,name64,name51,name51,name52,name52