java 8 stream
本篇文章主要讲如何使用Java8 的流库
流的概念
流表面上和集合很类似,都可以让我们转换和获取数据。但是,它们之间存在着显著的差异
- 流并不存储其元素。这些元素可能存储在底层的集合中,或者是按需生成的。
- 流的操作不会修改其数据源。比如,filter方法不会从原来的流中移除数据,而是生成一个不包含被过滤掉的元素的新的流,
- 流的操作是尽可能惰性执行的。这意味着直至需要其结果时,操作才会执行;如下面的代码如果我们只想查找前5个长度大于5的单词而不是所有,那么filter方法就会在匹配搭配第5个单词后停止过滤。
public static void main(String[] args) {
String words = "word,main,args,static,smile,happy,high,low,good,bad" ;
List<String> list = Arrays.asList(words.split(","));
long count = 0;
for(int i =0;i<list.size();i++){
if(list.get(i).length()>5){
count ++;
}
}
System.out.println(count);
count =list.stream().filter(s -> s.length() > 3).count();
System.out.println(count);
count =list.parallelStream().filter(s -> s.length() > 3).count();
System.out.println(count);
}
filter 产生一个流,其中包含满足其条件的所有元素
count 产生当前流中元素的数量。
stream 和 parallelStream 的区别就是parallelStream是并行流,stream 是顺序流。
让我们在看看上面的代码,stream方法会产生一个用于list的stream,filter会产生另一个流,其中只包含长度大于3的单词,count方法将这个流化简为一个结果。
这个工作流是操作流时的典型流程,我们建立了一个包含三个阶段的操作管道:
1.创建一个流。
2.指定将初始流转换为其他流的中间操作,可能包含多个步骤。
3.应用终止操作,从而产生结果。这个操作会强制执行之前的惰性操作。之后这个流就不能用了。
创建流的方式(概括)
1.Collection.stream()
Collection.parallelStream()
2.Stream.of() 产生一个元素为给定值的流
3.Stream.empty() 产生一个不包含任何元素的流
4.Stream.generate(Suppiler s)
通过实现 Supplier 接口,你可以自己来控制流的生成。这种情形通常用于随机数、常量的 Stream,或者需要前后元素间维持着某种状态信息的 Stream。把 Supplier 实例传递给 Stream.generate() 生成的 Stream,默认是串行(相对 parallel 而言)但无序的(相对 ordered 而言)。由于它是无限的,在管道中,必须利用 limit 之类的操作限制 Stream 大小。
例 生成十个随机数
Random seed = new Random();
Supplier<Integer> random = seed::nextInt;
Stream.generate(random).limit(10).forEach(System.out::println);
Stream.generate() 还接受自己实现的 Supplier。例如在构造海量测试数据的时候,用某种自动的规则给每一个变量赋值;或者依据公式计算 Stream 的每个元素值。这些都是维持状态信息的情形
例
Stream.generate(new PersonSupplier()).
limit(10).
forEach(p -> System.out.println(p.getName() + ", " + p.getAge()));
private class PersonSupplier implements Supplier<Person> {
private int index = 0;
private Random random = new Random();
@Override
public Person get() {
return new Person(index++, "StormTestUser" + index, random.nextInt(100));
}
}
输出结果
StormTestUser1, 9
StormTestUser2, 12
StormTestUser3, 88
StormTestUser4, 51
StormTestUser5, 22
StormTestUser6, 28
StormTestUser7, 81
StormTestUser8, 51
StormTestUser9, 4
StormTestUser10, 76
Stream.iterate(T seed ,UnaryOperator f)
产生一个无限流,它的元素包含种子,在种子上调用f产生的值、在前一个元素上调用f产生的值,然后种子值成为 Stream 的第一个元素,f(seed) 为第二个,f(f(seed)) 第三个,以此类推。
例 生成一个等差数列
Stream.iterate(0, n -> n + 3).limit(10). forEach(x -> System.out.print(x + " "));
结果 0 3 6 9 12 15 18 21 24 27
与 Stream.generate 相仿,在 iterate 时候管道必须有 limit 这样的操作来限制 Stream 大小。
流的操作
接下来,当把一个数据结构包装成 Stream 后,就要开始对里面的元素进行各类操作了。常见的操作可以归类如下。
intermediate 中间操作
该操作会保持 stream 处于中间状态,允许做进一步的操作。它返回的还是的 Stream,允许更多的链式操作。常见的中间操作有:
filter():对元素进行过滤;
sorted():对元素排序;
map():元素的映射;
distinct():去除重复元素;
limit(): 截取元素;
skip (n):当前流中跳过前n个元素之外的所有元素;
concat():连接两个流的元素;
Terminal 终止操作
该操作必须是流的最后一个操作,一旦被调用,Stream 就到了一个终止状态,而且不能再使用了。常见的终止操作有:
max:最大元素;
min:最小元素;
findFirst():第一个元素;
findAny():任意一个元素;
anyMatch,allMatch,noneMatch:分别在这个流中任意元素、所有元素和没有任何元素匹配给定断言是返回true。
并行流
流使得并行处理块的操作更容易。这个过程几乎是自动饿的,但是需要遵守一些规则。首先必须有一个并行流。可以用Collection.parallelStream方法从任何集合中获取一个并行流:
Stream<String> parallelList = list.parallelStream();
而且,parallel 方法可以将任意的顺序流转换为并行流
Stream<String> paralleList = Stream.of(list).parallel();
只要在终结方法执行时,流处于并行模式,那么所有的中间流操作都将被并行化。当流操作并行运行时,其目标是要让其返回结果与顺序执行时返回的结果相同,重要的是,这些操作可以按任意顺序执行。
下面是一个示例,假设你想要对字符串流中的所有短单词计数
String word = "word,main,args,static,smile,happy,high,low,good,bad" ;
List<String> wordList = Arrays.asList(word.split(","));
int[] shortWords = new int[12];
wordList.parallelStream().forEach(
s -> {if(s.length() <5) {
shortWords[s.length()]++;
}}
);
System.out.println(Arrays.toString(shortWords));
按上面的方法执行,你会发现每次运行产生的结果都不一样,而且每个数都是错的,这是因为传递给forEach 的函数会在多个并发线程中运行,每个都会更新共享的数组。但是如果我们如果用长度将字符串分组,然后分别对它们进行计数,那么就可以安全的并行化
Map<Integer, Long> collect = wordList.parallelStream().filter(s -> s.length() < 5)
.collect(Collectors.groupingBy(String::length, Collectors.counting()));
System.out.println(collect);
当放弃排序需求时,有些操作可以被更有效的并行化。通过在流上调用unordered方法就可以明确表示我们对排序不敢兴趣。distinct就是可以应用的一种操作。在有序的流中,distinct 会保留所有相同元素中的第一个,这对并行化是一种阻碍,因为处理每个部分的线程在之前的所有部分被处理完之前,并不知道应该丢弃那些元素。如果可以接受保留唯一元素中的任意一个的做法,那么所有部分就可以并行的处理。
还可以通过放弃排序要求来提高limit 方法的速度。如果只想从流中取出任意n个元素,而并不在意到底要获取那些,那么可以调用:
Stream<String> limit = wordList.parallelStream().unordered().limit(3);
当然 并不是所有的都需要并行流,当对内存中的数据执行大量计算操作是,才应该使用并行流。否则没有什么意思。
总结
总之,Stream 的特性可以归纳为:
- 不是数据结构
- 它没有内部存储,它只是用操作管道从 source(数据结构、数组、generator function、IO channel)抓取数据。
- 它也绝不修改自己所封装的底层数据结构的数据。例如 Stream 的 filter 操作会产生一个不包含被过滤元素的新 Stream,而不是从 source 删除那些元素。
- 所有 Stream 的操作必须以 lambda 表达式为参数
- 不支持索引访问;你可以请求第一个元素,但无法请求第二个,第三个,或最后一个。不过请参阅下一项。
- 很容易生成数组或者 List
- 惰性化;很多 Stream 操作是向后延迟的,一直到它弄清楚了最后需要多少数据才会开始。Intermediate 操作永远是惰性化的。
- 并行能力;当一个 Stream 是并行化的,就不需要再写多线程代码,所有对它的操作会自动并行进行的。
- 可以是无限的;集合有固定大小,Stream 则不必。limit(n) 和 findFirst() 这类的操作可以对无限的 Stream 进行运算并很快完成。