百日筑基第二十六天-java8 Stream数据流
之前没太在意这个知识点,直到看到公司里大家做遍历的时候都在用,而我看不太懂。。。。
简介
Stream
是Java SE 8
类库中新增的关键抽象,它被定义于java.util.stream
(这个包里有若干流类型:Stream<T>
代表对象引用流,此外还有一系列特化流,如IntStream
,LongStream
,DoubleStream
等。
Java 8
引入的的Stream
主要用于取代部分Collection
的操作,每个流代表一个值序列,流提供一系列常用的聚集操作,可以便捷的在它上面进行各种运算。
集合类库也提供了便捷的方式使我们可以以操作流的方式使用集合、数组以及其它数据结构;
Stream 特点
对Collection
进行处理,一般会使用Iterator
遍历器的遍历方式,这是一种外部迭代;
而对于处理Stream
,只要申明处理方式,处理过程由流对象自行完成,这是一种内部迭代,对于大量数据的迭代处理中,内部迭代比外部迭代要更加高效;
ordersList.stream()
.filter(r -> r != null)
.collect(Collectors.groupingBy(AiOrders::getOrderID))
.entrySet()
.iterator()
.forEachRemaining(r -> {
// r.getkey 分组的key r.getValue分组后的List值
if (r != null && CollectionUtils.isNotEmpty(r.getValue())) {
// 业务逻辑处理
}
});
Stream 相对于 Collection的优点
【1】无存储:流并不存储值;流的元素来自数据源(可能是某个数据结构、生成函数或I/O
通道等等),通过一系列计算步骤得到;
【2】代码简练: 对于一些collection
的迭代处理操作,使用stream
编写可以十分简洁,如果使用传统的collection
迭代操作,代码可能十分啰嗦,可读性也会比较糟糕;
【3】函数式风格:对流的操作会产生一个结果,但流的数据源不会被修改;
【4】惰性求值:多数流操作(包括过滤、映射、排序以及去重)都可以以惰性方式实现。这使得我们可以用一遍遍历完成整个流水线操作,并可以用短路操作提供更高效的实现;
【5】无需上界:不少问题都可以被表达为无限流(infinite stream):用户不停地读取流直到满意的结果出现为止(比如说,枚举这个操作可以被表达为在所有整数上进行过滤);集合是有限的,但流可以表达为无线流;
Stream 和 Iterator效率对比
传统iterator (for-loop)
比stream(JDK8)
迭代性能要高,尤其在小数据量的情况下;
在多核情景下,对于大数据量的处理,parallel stream
可以有比iterator
更高的迭代处理效率;
Stream 流是如何工作的?
流表示包含着一系列元素的集合,我们可以对其做不同类型的操作,用来对这些元素执行计算。听上去可能有点拗口,让我们用代码说话:
代码解读复制代码List<String> myList =
Arrays.asList("a1", "a2", "b1", "c2", "c1");
myList
.stream() // 创建流
.filter(s -> s.startsWith("c")) // 执行过滤,过滤出以 c 为前缀的字符串
.map(String::toUpperCase) // 转换成大写
.sorted() // 排序
.forEach(System.out::println); // for 循环打印
我们可以对流进行中间操作或者终端操作。小伙伴们可能会疑问?什么是中间操作?什么又是终端操作?
- ①:中间操作会再次返回一个流,所以,我们可以链接多个中间操作,注意这里是不用加分号的。上图中的
filter
过滤,map
对象转换,sorted
排序,就属于中间操作。 - ②:终端操作是对流操作的一个结束动作,一般返回
void
或者一个非流的结果。上图中的forEach
循环 就是一个终止操作。
看完上面的操作,感觉是不是很像一个流水线式操作呢。
实际上,大部分流操作都支持 lambda 表达式作为参数,正确理解,应该说是接受一个函数式接口的实现作为参数。
不同类型的 Stream 流
我们可以从各种数据源中创建 Stream 流,其中以 Collection 集合最为常见。如 List
和 Set
均支持 stream()
方法来创建顺序流或者是并行流。
并行流是通过多线程的方式来执行的,它能够充分发挥多核 CPU 的优势来提升性能。本文在最后再来介绍并行流,我们先讨论顺序流:
Arrays.asList("a1", "a2", "a3")
.stream() // 创建流
.findFirst() // 找到第一个元素
.ifPresent(System.out::println); // 如果存在,即输出
在集合上调用stream()
方法会返回一个普通的 Stream 流。但是, 您大可不必刻意地创建一个集合,再通过集合来获取 Stream 流,您还可以通过如下这种方式:
Stream.of("a1", "a2", "a3")
.findFirst()
.ifPresent(System.out::println);
例如上面这样,我们可以通过 Stream.of()
从一堆对象中创建 Stream 流。
除了常规对象流之外,Java 8还附带了一些特殊类型的流,用于处理原始数据类型int
,long
以及double
。说道这里,你可能已经猜到了它们就是IntStream
,LongStream
还有DoubleStream
。
其中,IntStreams.range()
方法还可以被用来取代常规的 for
循环, 如下所示:
IntStream.range(1, 4)
.forEach(System.out::println); // 相当于 for (int i = 1; i < 4; i++) {}
// 1
// 2
// 3
上面这些原始类型流的工作方式与常规对象流基本是一样的,但还是略微存在一些区别:
- 原始类型流使用其独有的函数式接口,例如
IntFunction
代替Function
,IntPredicate
代替Predicate
。 - 原始类型流支持额外的终端聚合操作,
sum()
以及average()
,如下所示:
Arrays.stream(new int[] {1, 2, 3})
.map(n -> 2 * n + 1) // 对数值中的每个对象执行 2*n + 1 操作
.average() // 求平均值
.ifPresent(System.out::println); // 如果值不为空,则输出
// 5.0
但是,偶尔我们也有这种需求,需要将常规对象流转换为原始类型流,这个时候,中间操作 mapToInt()
,mapToLong()
以及mapToDouble
就派上用场了:
Stream.of("a1", "a2", "a3")
.map(s -> s.substring(1)) // 对每个字符串元素从下标1位置开始截取
.mapToInt(Integer::parseInt) // 转成 int 基础类型类型流
.max() // 取最大值
.ifPresent(System.out::println); // 不为空则输出
// 3
如果说,您需要将原始类型流装换成对象流,您可以使用 mapToObj()
来达到目的:
IntStream.range(1, 4)
.mapToObj(i -> "a" + i) // for 循环 1->4, 拼接前缀 a
.forEach(System.out::println); // for 循环打印
// a1
// a2
// a3
下面是一个组合示例,我们将双精度流首先转换成 int
类型流,然后再将其装换成对象流:
Stream.of(1.0, 2.0, 3.0)
.mapToInt(Double::intValue) // double 类型转 int
.mapToObj(i -> "a" + i) // 对值拼接前缀 a
.forEach(System.out::println); // for 循环打印
// a1
// a2
// a3
Stream 流的处理顺序
上小节中,我们已经学会了如何创建不同类型的 Stream 流,接下来我们再深入了解下数据流的执行顺序。
在讨论处理顺序之前,您需要明确一点,那就是中间操作的有个重要特性 —— 延迟性。观察下面这个没有终端操作的示例代码:
Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> {
System.out.println("filter: " + s);
return true;
});
执行此代码段时,您可能会认为,将依次打印 “d2”, “a2”, “b1”, “b3”, “c” 元素。然而当你实际去执行的时候,它不会打印任何内容。
为什么呢?
原因是:当且仅当存在终端操作时,中间操作操作才会被执行。
是不是不信?接下来,对上面的代码添加 forEach
终端操作:
Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> {
System.out.println("filter: " + s);
return true;
})
.forEach(s -> System.out.println("forEach: " + s));
再次执行,我们会看到输出如下:
filter: d2
forEach: d2
filter: a2
forEach: a2
filter: b1
forEach: b1
filter: b3
forEach: b3
filter: c
forEach: c
输出的顺序可能会让你很惊讶!你脑海里肯定会想,应该是先将所有 filter
前缀的字符串打印出来,接着才会打印 forEach
前缀的字符串。
事实上,输出的结果却是随着链条垂直移动的。比如说,当 Stream 开始处理 d2 元素时,它实际上会在执行完 filter 操作后,再执行 forEach 操作,接着才会处理第二个元素。
是不是很神奇?为什么要设计成这样呢?
原因是出于性能的考虑。这样设计可以减少对每个元素的实际操作数,看完下面代码你就明白了:
Stream.of("d2", "a2", "b1", "b3", "c")
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase(); // 转大写
})
.anyMatch(s -> {
System.out.println("anyMatch: " + s);
return s.startsWith("A"); // 过滤出以 A 为前缀的元素
});
// map: d2
// anyMatch: D2
// map: a2
// anyMatch: A2
终端操作 anyMatch()
表示任何一个元素以 A 为前缀,返回为 true
,就停止循环。所以它会从 d2
开始匹配,接着循环到 a2
的时候,返回为 true
,于是停止循环。
由于数据流的链式调用是垂直执行的,map
这里只需要执行两次。相对于水平执行来说,map
会执行尽可能少的次数,而不是把所有元素都 map
转换一遍。
中间操作顺序这么重要?
下面的例子由两个中间操作map
和filter
,以及一个终端操作forEach
组成。让我们再来看看这些操作是如何执行的:
Stream.of("d2", "a2", "b1", "b3", "c")
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase(); // 转大写
})
.filter(s -> {
System.out.println("filter: " + s);
return s.startsWith("A"); // 过滤出以 A 为前缀的元素
})
.forEach(s -> System.out.println("forEach: " + s)); // for 循环输出
// map: d2
// filter: D2
// map: a2
// filter: A2
// forEach: A2
// map: b1
// filter: B1
// map: b3
// filter: B3
// map: c
// filter: C
学习了上面一小节,您应该已经知道了,map
和filter
会对集合中的每个字符串调用五次,而forEach
却只会调用一次,因为只有 “a2” 满足过滤条件。
如果我们改变中间操作的顺序,将filter
移动到链头的最开始,就可以大大减少实际的执行次数:
Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> {
System.out.println("filter: " + s)
return s.startsWith("a"); // 过滤出以 a 为前缀的元素
})
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase(); // 转大写
})
.forEach(s -> System.out.println("forEach: " + s)); // for 循环输出
// filter: d2
// filter: a2
// map: a2
// forEach: A2
// filter: b1
// filter: b3
// filter: c
现在,map
仅仅只需调用一次,性能得到了提升,这种小技巧对于流中存在大量元素来说,是非常很有用的。
接下来,让我们对上面的代码再添加一个中间操作sorted
:
Stream.of("d2", "a2", "b1", "b3", "c")
.sorted((s1, s2) -> {
System.out.printf("sort: %s; %s\n", s1, s2);
return s1.compareTo(s2); // 排序
})
.filter(s -> {
System.out.println("filter: " + s);
return s.startsWith("a"); // 过滤出以 a 为前缀的元素
})
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase(); // 转大写
})
.forEach(s -> System.out.println("forEach: " + s)); // for 循环输出
sorted
是一个有状态的操作,因为它需要在处理的过程中,保存状态以对集合中的元素进行排序。
执行上面代码,输出如下:
sort: a2; d2
sort: b1; a2
sort: b1; d2
sort: b1; a2
sort: b3; b1
sort: b3; d2
sort: c; b3
sort: c; d2
filter: a2
map: a2
forEach: A2
filter: b1
filter: b3
filter: c
filter: d2
咦咦咦?这次怎么又不是垂直执行了。你需要知道的是,sorted
是水平执行的。因此,在这种情况下,sorted
会对集合中的元素组合调用八次。这里,我们也可以利用上面说道的优化技巧,将 filter 过滤中间操作移动到开头部分:
Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> {
System.out.println("filter: " + s);
return s.startsWith("a");
})
.sorted((s1, s2) -> {
System.out.printf("sort: %s; %s\n", s1, s2);
return s1.compareTo(s2);
})
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase();
})
.forEach(s -> System.out.println("forEach: " + s));
// filter: d2
// filter: a2
// filter: b1
// filter: b3
// filter: c
// map: a2
// forEach: A2
从上面的输出中,我们看到了 sorted
从未被调用过,因为经过filter
过后的元素已经减少到只有一个,这种情况下,是不用执行排序操作的。因此性能被大大提高了。
数据流复用问题
Java8 Stream 流是不能被复用的,一旦你调用任何终端操作,流就会关闭:
Stream<String> stream =
Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> s.startsWith("a"));
stream.anyMatch(s -> true); // ok
stream.noneMatch(s -> true); // exception
当我们对 stream 调用了 anyMatch
终端操作以后,流即关闭了,再调用 noneMatch
就会抛出异常:
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.noneMatch(ReferencePipeline.java:459)
at com.winterbe.java8.Streams5.test7(Streams5.java:38)
at com.winterbe.java8.Streams5.main(Streams5.java:28)
为了克服这个限制,我们必须为我们想要执行的每个终端操作创建一个新的流链,例如,我们可以通过 Supplier
来包装一下流,通过 get()
方法来构建一个新的 Stream
流,如下所示:
Supplier<Stream<String>> streamSupplier =
() -> Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> s.startsWith("a"));
streamSupplier.get().anyMatch(s -> true); // ok
streamSupplier.get().noneMatch(s -> true); // ok