Java 8 Stream
Java 8 API添加了一个新的抽象称为流Stream,可以以一种声明的方式处理数据。
这种风格将要处理的元素集合看作一种流, 流在管道中传输, 并且可以在管道的节点上进行处理, 比如筛选, 排序,聚合等。
元素流在管道中经过中间操作的处理,最后由最终操作得到前面处理的结果。
什么是 Stream?
Stream(流)是一个来自数据源的元素队列并支持聚合操作
- 元素是特定类型的对象,形成一个队列。 Java中的Stream并不会存储元素,而是按需计算。
- 数据源 流的来源。 可以是集合,数组,I/O channel, 产生器generator 等。
- 聚合操作 类似SQL语句一样的操作, 比如filter, map, reduce, find, match, sorted等。
和以前的Collection操作不同, Stream操作还有两个基础的特征:
- Pipelining: 中间操作都会返回流对象本身。 这样多个操作可以串联成一个管道, 如同流式风格(fluent style)。 这样做可以对操作进行优化, 比如延迟执行(laziness)和短路( short-circuiting)。
- 内部迭代: 以前对集合遍历都是通过Iterator或者For-Each的方式, 显式的在集合外部进行迭代, 这叫做外部迭代。 Stream提供了内部迭代的方式, 通过访问者模式(Visitor)实现。
生成流
在 Java 8 中, 集合接口有两个方法来生成流:
- stream() − 为集合创建串行流。
- parallelStream() − 为集合创建并行流。
forEach
Stream 提供了新的方法 ‘forEach’ 来迭代流中的每个数据。以下代码片段使用 forEach 输出了10个随机数:
Random random = new Random();
random.ints().limit(10).forEach(System.out::println);
map
map 方法用于映射每个元素到对应的结果,以下代码片段使用 map 输出了元素对应的平方数:
List<Integer> numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);
// 获取对应的平方数
List<Integer> squaresList = numbers.stream()
.map( i -> i*i).distinct().collect(Collectors.toList());
filter
filter 方法用于通过设置的条件过滤出元素。以下代码片段使用 filter 方法过滤出空字符串:
List<String>strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");
// 获取空字符串的数量
long count = strings.stream().filter(string -> string.isEmpty()).count();
limit
limit 方法用于获取指定数量的流。 以下代码片段使用 limit 方法打印出 10 条数据:
Random random = new Random(); random.ints().limit(10).forEach(System.out::println);
sorted
sorted 方法用于对流进行排序。以下代码片段使用 sorted 方法对输出的 10 个随机数进行排序:
Random random = new Random(); random.ints().limit(10).sorted().forEach(System.out::println);
Collectors
Collectors 类实现了很多归约操作,例如将流转换成集合和聚合元素。Collectors 可用于返回列表或字符串:
List<String>strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");
List<String> filtered = strings.stream()
.filter(string -> !string.isEmpty()).collect(Collectors.toList());
System.out.println("筛选列表: " + filtered);
String mergedString = strings.stream()
.filter(string -> !string.isEmpty())
.collect(Collectors.joining(", "));
System.out.println("合并字符串: " + mergedString);
reduce
元素求和
如何使用for-each循环来对数字列表中的 元素求和:
int sum = 0;
for (int x : numbers) {
sum += x;
}
numbers中的每个元素都用加法运算符反复迭代来得到结果。通过反复使用加法,把一个数字列表归约成了一个数字。
reduce对这种重复应用的模式做了抽象。你可以像下面这样对流中所有的元素求和:
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
reduce接受两个参数:
- 一个初始值,这里是0;
- 一个BinaryOperator来将两个元素结合起来产生一个新值,这里我们用的是 lambda (a, b) -> a + b。
你也很容易把所有的元素相乘,只需要将另一个Lambda:(a, b) -> a * b传递给reduce 操作就可以了:
int product = numbers.stream().reduce(1, (a, b) -> a * b);
在Java 8中,Integer类现在有了一个静态的sum 方法来对两个数求和,这恰好是我们想要的,用不着反复用Lambda写同一段代码了:
int sum = numbers.stream().reduce(0, Integer::sum);
无初始值
reduce还有一个重载的变体,它不接受初始值,但是会返回一个Optional对象:
Optional sum = numbers.stream().reduce((a, b) -> (a + b));
为什么它返回一个Optional呢?考虑流中没有任何元素的情况。reduce操作无法返回其和,因为它没有初始值。这就是为什么结果被包裹在一个Optional对象里,以表明和 可能不存在。
最大值和最小值
给定两个元素能够返回最大值的Lambda。reduce操作会考虑新值和流中下一个元素,并产生一 个新的最大值:
Optional<Integer> max = numbers.stream().reduce(Integer::max);
要计算最小值,你需要把Integer.min传给reduce来替换Integer.max:
Optional min = numbers.stream().reduce(Integer::min);
你当然也可以写成Lambda (x, y) -> x < y ? x : y而不是Integer::min,不过后者 比较易读。
数值流
在前面看到了可以使用reduce方法计算流中元素的总和
int sum = userList.stream().map(User::getAge).reduce(0, Integer::sum);
这段代码的问题是,它有一个暗含的装箱成本。每个Integer都必须拆箱成一个原始类型, 再进行求和。
原始类型流特化
Java 8引入了三个原始类型特化流接口来解决这个问题:IntStream、DoubleStream和 LongStream,分别将流中的元素特化为int、long和double,从而避免了暗含的装箱成本。每 个接口都带来了进行常用数值归约的新方法,比如对数值流求和的sum,找到最大元素的max。 此外还有在必要时再把它们转换回对象流的方法。要记住的是,这些特化的原因并不在于流的复杂性,而是装箱造成的复杂性——即类似int和Integer之间的效率差异
映射到数值流
将流转换为特化版本的常用方法是mapToInt、mapToDouble和mapToLong。这些方法和前 面说的map方法的工作方式一样,只是它们返回的是一个特化流,而不是Stream。
int calories = userList.stream().mapToInt(User::Age).sum();
这里,mapToInt会从每个用户提取年龄(用一个Integer表示),并返回一个IntStream (而不是一个Stream)。然后你就可以调用IntStream接口中定义的sum方法,对年龄求和了!请注意,如果流是空的,sum默认返回0。IntStream还支持其他的方便方法,如 max、min、average等。
转换回对象流
同样,一旦有了数值流,你可能会想把它转换回非特化流。例如,IntStream上的操作只能 产生原始整数: IntStream 的 map 操作接受的 Lambda 必须接受 int 并返回 int (一个 IntUnaryOperator)。但是你可能想要生成另一类值,比如Dish。为此,你需要访问Stream 接口中定义的那些更广义的操作。要把原始流转换成一般流(每个int都会装箱成一个 Integer),可以使用boxed方法,如下所示:
IntStream intStream = userList.stream().mapToInt(User::getAge);
Stream<Integer> stream = intStream.boxed();
默认值OptionalInt
求和的那个例子很容易,因为它有一个默认值:0。但是,如果你要计算IntStream中的最 大元素,就得换个法子了,因为0是错误的结果。如何区分没有元素的流和最大值真的是0的流呢? 前面我们介绍了Optional类,这是一个可以表示值存在或不存在的容器。Optional可以用 Integer、String等参考类型来参数化。对于三种原始流特化,也分别有一个Optional原始类 型特化版本:OptionalInt、OptionalDouble和OptionalLong。 例如,要找到IntStream中的最大元素,可以调用max方法,它会返回一个OptionalInt:
OptionalInt maxCalories = userList.stream().mapToInt(User::getAge).max();
现在,如果没有最大值的话,你就可以显式处理OptionalInt去定义一个默认值了:
int max = maxCalories.orElse(1);
并行(parallel)程序
parallelStream 是流并行处理程序的代替方法。以下实例我们使用 parallelStream 来输出空字符串的数量:
List<String> strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");
// 获取空字符串的数量
long count = strings.parallelStream().filter(string -> string.isEmpty()).count();
我们可以很容易的在顺序运行和并行直接切换。
性能测试
//串行流
long startTime1 = System.currentTimeMillis();
Stream.iterate(1,i -> i+1)
.limit(1000_0000)
.reduce(0,Integer::sum);
long endTime1 = System.currentTimeMillis();
System.out.println("stream串行流执行时间:" + (endTime1 - startTime1) + "ms");
//并行流
long startTime2 = System.currentTimeMillis();
Stream.iterate(1,i -> i+1)
.limit(1000_0000)
.parallel()
.reduce(0,Integer::sum);
long endTime2 = System.currentTimeMillis();
System.out.println("parallelStream并行流执行时间:" + (endTime2 - startTime2) + "ms");
执行结果:
stream串行流执行时间:717ms
parallelStream并行流执行时间:4488ms
求和方法的并行版本比顺序版本要慢很多
- iterate生成的是装箱的对象,必须拆箱成数字才能求和
- 很难把iterate分成多个独立块来并行执行
具体来 说,iterate很难分割成能够独立执行的小块,因为每次应用这个函数都要依赖前一次应用的结果
这就说明了并行编程可能很复杂,有时候甚至有点违反直觉。如果用得不对(比如采用了一 个不易并行化的操作,如iterate),它甚至可能让程序的整体性能更差
使用更有针对性的方法
IntStream.rangeClosed直接产生原始类型的long数字,没有装箱拆箱的开销
IntStream.rangeClosed会生成数字范围,很容易拆分为独立的小块。例如,范围1~20 可分为1~5 、6~10 、11~15 和16~20。
long startTime3 = System.currentTimeMillis();
IntStream.rangeClosed(1,1000_0000).reduce(0,Integer::sum);
long endTime3 = System.currentTimeMillis();
System.out.println("stream串行流执行时间:" + (endTime3 - startTime3) + "ms");
long startTime4 = System.currentTimeMillis();
IntStream.rangeClosed(1,1000_0000).parallel().reduce(0,Integer::sum);
long endTime4 = System.currentTimeMillis();
System.out.println("parallelStream并行流执行时间:" + (endTime4 - startTime4) + "ms");
执行结果:
stream串行流执行时间:201ms
parallelStream并行流执行时间:41ms
并行化过程本身需要对流做递归划分,把每 个子流的归纳操作分配到不同的线程,然后把这些操作的结果合并成一个值。但在多个内核之间 移动数据的代价也可能比你想的要大,所以很重要的一点是要保证在内核中并行执行工作的时间比在内核之间传输数据的时间长
注意
- 留意装箱。自动装箱和拆箱操作会大大降低性能。Java 8中有原始类型流(IntStream、 LongStream、DoubleStream)来避免这种操作,但凡有可能都应该用这些流。
- 有些操作本身在并行流上的性能就比顺序流差。特别是limit和findFirst等依赖于元 素顺序的操作,它们在并行流上执行的代价非常大。例如,findAny会比findFirst性 能好,因为它不一定要按顺序来执行。你总是可以调用unordered方法来把有序流变成 无序流。那么,如果你需要流中的n个元素而不是专门要前n个的话,对无序并行流调用 limit可能会比单个有序流(比如数据源是一个List)更高效。
- 要考虑流背后的数据结构是否易于分解。例如,ArrayList的拆分效率比LinkedList 高得多,因为前者用不着遍历就可以平均拆分,而后者则必须遍历。