Stream流
Stream流是Java API的新成员,它允许声明性方式处理数据集合,可以把它们看成遍历数据集的高级迭代器。
通常在集合的相关操作中,一般有存储、迭代、计算三个方面的功能,在Java8之前一般是用集合进行存储,用迭代器进行外部迭代和计算,而Stream的引入,是将计算的功能拆分出来。
集合和流的区别
粗略地说,集合与流之间的差异就在于什么时候进行计算。集合使用的时候所有元素都在内存中,流使用的时候才会出现。从另一个角度来说,流就像是一个延迟创建的集合
- 集合是一个内存中的数据结构,它包含数据结构中目前所有的值——集合中的每个元素都得先算出来才能添加到集合中
- 流则是在概念上固定的数据结构(你不能添加或删除元素),其元素则是按需计算的。只会按需生成
- 外部迭代与内部迭代。Streams库的内部迭代可以自动选择一种适合你硬件的数据表示和并行实现;外部迭代需要自己进行处理
相关概念
元素序列:就像集合一样,流也提供了一个接口,可以访问特定元素类型的一组有序值
源:流会使用一个提供数据的源,如集合、数组或输入/输出资源。请注意,从有序集合生成流时会保留原有的顺序。由列表生成的流,其元素顺序与列表一致。
数据处理操作:流的数据处理功能支持类似于数据库的操作,以及函数式编程语言中的常用操作,如filter、map、reduce、find、match、sort等。流操作可以顺序执行,也可并行执行。
流水线:很多流操作本身会返回一个流,这样多个操作就可以链接起来,形成一个大的流水线
内部迭代:与集合使用迭代器显式迭代的集合不同,流的迭代操作是内部进行的,我们不需要去进行迭代控制
Stream优点
- 声明性。代码是以声明性方式写的,更简洁,更易读
- 可复合。你可以把几个基础操作链接起来,来表达复杂的数据处理流水线。同时保持代码清晰可读
- 可并行。因为filter、sorted、map和collect等操作是与具体线程模型无关的高层次构件,所以它们的内部实现可以是单线程的,也可能透明地充分利用你的多核架构
构建流
- 集合创建流
List<String> list = new ArrayList<>();
list.stream();
- 值创建流
Stream<String> stream = Stream.of("Java 8 ", "Lambdas ", "In ", "Action");
- 数组创建流
int[] numbers = {2, 3, 5, 7, 11, 13};
Arrays.stream(numbers)
- 文件生成流。使用Files.lines得到一个流,其中的每个元素都是给定文件中的一行
Stream<String> stream = Files.lines(Paths.get("test.txt"));
- 由函数生成流。Stream.iterate和Stream.generate。这两个操作可以创建所谓的无限流:不像从固定集合创建的流那样有固定大小的流
Stream.iterate(1, n->n+1).limit(10).forEach(System.out::println);
使用流的常见方法
Stream类中的方法分为中间操作和终端操作。中间操作是用来组装流水线,终端操作是使用流水线,消耗元素并产生结果。
中间操作
除非流水线上触发一个终端操作,否则中间操作不会执行任何处理
- filter。接受Lambda,从流中排除某些元素
- map。接受一个Lambda,将元素转换成其他形式或提取信息
- mapToInt、mapToLong等。避免装箱的map操作
- limit。截断流,使其元素不超过给定数量。
- distinct。返回一个元素各异(根据流所生成元素的hashCode和equals方法实现)的流
- skip返回一个扔掉了前n个元素的流。
- flatmap方法让你把一个流中的每个值都换成另一个流,然后把所有的流连接起来成为一个流。
- iterate传入初始值和迭代方法创建无限流,可以通过limit进行长度限制
- peek。接收一个Lambda,为每个元素执行函数操作
- sorted。为流按照默认顺序或者接收Lambda进行排序
终端操作
终端操作会从流的流水线生成结果
- collect。将流转换为其他形式。
- 查找和匹配:allMatch、anyMatch、noneMatch、findFirst和findAny
- anyMatch.anyMatch方法可以回答“流中是否有一个元素能匹配给定的谓词”
- allMatch.流中的元素是否都能匹配给定的谓语
- noneMatch.确保流中没有任何元素与给定的谓词匹配
- findAny方法将返回当前流中的任意元素
- reduce.将流中所有元素反复结合起来,得到一个值
- max、min、sum、count
收集器
我们对集合的遍历操作,要么是为每个元素执行特定方法,要么是消费掉元素产生特定类型的结果。在产生结果的时候一般采用collect方法,它接收一个Collector对象,里面定义了如何对数据进行归约操作。
预定义收集器
Collectors中提供了很多有用的预定义收集器,它们分为下面三大类
- 将流元素归约和汇总为一个值
- 元素分组
- 元素分区
常见的预定义收集器
- counting
- maxBy、minBy
- averagingInt、summingInt、
- summarizingInt
- joining
- reducing
- groupingBy
- collectingAndThen
- partitioningBy
收集器接口
Collector接口包含了一系列方法,为实现具体的归约操作(即收集器)提供了范本
// T是流中要收集的项目的泛型。
// A是累加器的类型,累加器是在收集过程中用于累积部分结果的对象。
// R是收集操作得到的对象(通常但并不一定是集合)的类型。
// public class ToListCollector<T> implements Collector<T, List<T>, List<T>>
public interface Collector<T, A, R> {
Supplier<A> supplier();
BiConsumer<A, T> accumulator();
BinaryOperator<A> combiner();
Function<A, R> finisher();
Set<Characteristics> characteristics();
...
- supplier方法必须返回一个结果为空的Supplier,也就是一个无参数函数,在调用时它会创建一个空的累加器实例,供数据收集过程使用
- accumulator方法会返回执行归约操作的函数
- 在遍历完流后,finisher方法必须返回在累积过程的最后要调用的一个函数,以便将累加器对象转换为整个集合操作的最终结果
- combiner方法会返回一个供归约操作使用的函数,它定义了对流的各个子部分进行并行处理时,各个子部分归约所得的累加器要如何合并
- characteristics会返回一个不可变的Characteristics集合,它定义了收集器的行为——尤其是关于流是否可以并行归约,以及可以使用哪些优化的提示
Stream并行
本部分简单介绍了Stream并行的部分内容,实际使用需要经过大量测试。
Stream针对并行操作提供了解决思路,因为是内部迭代,它的并行实现不需要我们去进行控制,因此实现起来非常简单。但是针对Stream的并行最好进行大量的测试,因为Stream中并不是所有的操作都适合并行,不适当的操作甚至不如顺序Stream流。
并行流内部使用默认的ForkJoinPool,它默认的线程数量就是处理器的数量。
System.out.println(Runtime.getRuntime().availableProcessors());
// 可以通过下面方法改变线程池大小,它将影响代码中所有的并行流
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism","12");
并行流的相关方法
- parallelStream方法将集合转换为并行流
- parallel可以将顺序流转变成并行流
- sequential可以将并行流转变成顺序流
针对并行计算的优化思路
在大量的循环中,首先尽量避免大量的装箱操作,比如calBySeq和calByLongStreamSeq;
合理的选择并行流的实现方式
package test;
import java.util.function.Function;
import java.util.stream.LongStream;
import java.util.stream.Stream;
public class Test {
public static void main(String... args) {
System.out.println("执行时间为" + measureSumPerf(Test::calByLongStreamPal, 10000000) + "毫秒");
}
public static long measureSumPerf(Function<Long, Long> adder, long n) {
long fastest = Long.MAX_VALUE;
for (int i = 0; i < 10; i++) {
long start = System.nanoTime();
long sum = adder.apply(n);
long duration = (System.nanoTime() - start) / 1_000_000;
System.out.println("Result: " + sum);
if (duration < fastest)
fastest = duration;
}
return fastest;
}
// 执行时间为85毫秒
public static long calBySeq(long n) {
return Stream.iterate(1L, i->i+1).limit(n).reduce(1L, Long::sum);
}
// 执行时间为142毫秒
public static long calByPal(long n) {
return Stream.iterate(1L, i->i+1).limit(n).parallel().reduce(1L, Long::sum);
}
// 执行时间为4毫秒;普通的迭代器方式,非并行
public static long calByIte(long n) {
long a =0;
for (long i=1;i<=n;i++) {
a += i;
}
return a;
}
// 执行时间为4毫秒
public static long calByLongStreamSeq(long n) {
return LongStream.range(1, n).reduce(1L, Long::sum);
}
// 执行时间为1毫秒
public static long calByLongStreamPal(long n) {
return LongStream.range(1, n).parallel().reduce(1L, Long::sum);
}
}
并行流注意要点
- 并行流并不总比顺序流快,要通过测试来校验
- 注意装箱带来的性能影响
- 注意操作,有些操作本身在并行流上就比顺序流差
- 要考虑流背后的数据结构是否容易分解,ArrayList比LinkedList分解要容易得多
[外链图片转存失败(img-lPq855U5-1562652278989)(31BB9B652A554E1191695F533AE8B743)] - 还要考虑终端操作中合并步骤的代价是大是小
- 保证在内核中并行执行工作的时间比在内核之间传输数据的时间长
新增Spliterator接口-可分迭代
和Iterator一样,Spliterator也用于遍历数据源中的元素,但它是为了并行执行而设计的。Java8已经为集合框架中包含的所有数据结构提供了一个默认的Spliterator实现。在实际使用中,可以将Spliterator与StreamSupport结合创建并行流Stream,由Spliterator来控制并行时拆分数据的策略。
public interface Spliterator<T> {
// 尝试对T执行函数
boolean tryAdvance(Consumer<? super T> action);
// 为每个T执行函数
default void forEachRemaining(Consumer<? super T> action) {
do { } while (tryAdvance(action));
}
// 对Spliterator进行拆分,不能拆分返回null
Spliterator<T> trySplit();
// 返回大致剩余元素数
long estimateSize();
// Spliterator本身特性集的编码
int characteristics();
}