前言
Stream 作为 Java 8 的一大亮点,它与 java.io 包里的 InputStream 和 OutputStream 是完全不同的概念。它也不同于 StAX 对 XML 解析的 Stream,也不是 Amazon Kinesis 对大数据实时处理的 Stream。Java 8 中的 Stream 是对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作(aggregate operation)(例如统计最大/最小值,统计总和等),或者大批量数据操作 (bulk data operation)。它提供串行和并行两种模式进行汇聚操作,并发模式能够充分利用多核处理器的优势,使用 fork/join 并行方式来拆分任务和加速处理过程。通常编写并行代码很难而且容易出错, 但使用 Stream API 无需编写一行多线程的代码,就可以很方便地写出高性能的并发程序。所以说,Java 8 中首次出现的 java.util.stream 是一个函数式语言+多核时代综合影响的产物。
Stream
1、Stream是什么?
Stream 不是集合元素,它不是数据结构并不保存数据,它是有关算法和计算的,它更像一个高级版本的 Iterator。原始版本的 Iterator,用户只能显式地一个一个遍历元素并对其执行某些操作;高级版本的 Stream,用户只要给出需要对其包含的元素执行什么操作,比如 “过滤掉长度大于 10 的字符串”、“获取每个字符串的首字母”等,Stream 会隐式地在内部进行遍历,做出相应的数据转换。
Stream有以下特点:
- 不存储数据。 流是基于数据源的对象,它本身不存储数据元素,而是通过管道将数据源的元素传递给操作。
- 函数式编程。 流的操作不会修改数据源,例如filter不会将数据源中的数据删除。
- 延迟操作。 流的很多操作如filter,map等中间操作是延迟执行的,只有到终点操作才会将操作顺序执行。
- 可以解绑。 对于无限数量的流,有些操作是可以在有限的时间完成的,比如limit(n) 或 findFirst(),这些操作可是实现"短路"(Short-circuiting),访问到有限的元素后就可以返回。
- 纯消费。 流的元素只能访问一次,类似Iterator,操作没有回头路,如果你想从头重新访问流的元素,对不起,你得重新生成一个新的流。
- 使用了Fork/Join。 Stream使用并行去遍历时,数据会被分成多个段,其中每一个都在不同的线程中处理,然后将结果一起输出
- Stream数据源本身是可以无限的
2、Stream的基本使用流程
当我们使用一个流的时候,通常包括三个基本步骤:
获取一个数据源(source)→ 数据转换→执行操作获取想要的结果,每次转换原有 Stream 对象不改变,返回一个新的 Stream 对象(可以有多次转换),这就允许对其操作可以像链条一样排列,变成一个管道。
3、获取一个数据源(Stream Source)的方式
- Collection 提供的stream()方法和parallelStream()方法
- Arrays类 提供的静态stream()方法可以将数组转换为流
- BufferedReader类 提供了lines()方法
- 使用流的静态方法,比如Stream.of(Object[]), IntStream.range(int, int) 或者 Stream.iterate(Object, UnaryOperator),如Stream.iterate(0, n -> n * 2),或者generate(Supplier s)如Stream.generate(Math::random)
- Files类的操作路径的方法,如list、find、walk等。
- 随机数流Random.ints()。
- 其它一些类提供了创建流的方法,如BitSet.stream(), Pattern.splitAsStream(java.lang.CharSequence), 和 JarFile.stream()。
- 更底层的使用StreamSupport,它提供了将Spliterator转换成流的方法。
4、流的操作
流有两种操作类型:
- Intermediate:一个流可以后面跟随零个或多个 intermediate 操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。
- Terminal:一个流只能有一个 terminal 操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果,或者一个 side effect。
例子:
Long[] nums = new Long[]{2L,6786L,362874L,22342L,1143241L};
long sum = Arrays.stream(nums)
.filter(n->n > 1000)
.mapToInt(n->n.intValue())
.sum();
上面的例子里,stream方法获取数据源,filter和mapToInt方法时intermediate操作,用来对流做过滤和数据映射,sum是terminal操作,用来计算总和。
5、流的详解
5.1、常用的流的构造方法
//方式一、使用Stream的静态方法of
Stream<String> stream1 = Stream.of("a", "b", "c");
//方式二、使用集合类的stream方法
List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream2 = list.stream();
//方式三、使用Arrays类的静态方法
Stream<String> stream3 =Arrays.stream(new String[]{"a","b","v"});
注意:对于基本数值型,目前有三种对应的包装类型 Stream:
- IntStream
- LongStream
- DoubleStream
使用方式如下(以LongStream为例):
LongStream stream = LongStream.builder()
.add(1L)
.add(2L)
.build();
LongStream stream2 = LongStream.of(new long[]{1L,2L});
LongStream stream3 = LongStream.range(1L,5L);
LongStream stream4 = LongStream.rangeClosed(1L,100L);
使用这种方式,而不是Stream< Long> stream = Stream.of…的方式是因为就装箱和拆箱效率来说,直接使用包装类型Stream的效率更高些。
5.2、Stream转换成数据结构
- toArray方法将一个流转换成数组
- Collectors.toXXX方法转换成其他数据结构
例子:
// 1. Array
String[] strArray1 = stream1.toArray(String[]::new);
// 2. Collection
List<String> list1 = stream2.collect(Collectors.toList());
List<String> list2 = stream3.collect(Collectors.toCollection(ArrayList::new));
Set set1 = stream4.collect(Collectors.toSet());
Stack stack1 = stream5.collect(Collectors.toCollection(Stack::new));
// 3. String
String str = stream6.collect(Collectors.joining()).toString();
5.3、intermediate操作
中间操作会返回一个新的流,并且操作是延迟执行的(lazy),它不会修改原始的数据源,而且是由在终点操作开始的时候才真正开始执行。
5.3.1、distinct
distinct保证输出的流中包含唯一的元素,它是通过Object.equals(Object)来检查是否包含相同的元素。
Stream<String> stream = Stream.of(new String[]{"a","b","c","b"});
List<String> list = stream.distinct().collect(Collectors.toList());
list.forEach(System.out::println);
5.3.2、filter
filter返回的流中只包含满足断言(predicate)的数据。
IntStream stream = IntStream.of(new int[] {10,20,30,40,50});
List<Integer> res = stream.filter(n->n>20).boxed().collect(Collectors.toList());
res.forEach(System.out::println);
5.3.3、map
map方法将流中的元素映射成另外的值,新的值类型可以和原来的元素的类型不同。
Stream<String> stream = Arrays.asList("10", "20", "30", "40").stream();
List<Integer> res = stream.mapToInt(str -> Integer.parseInt(str))
.filter(n -> n > 20)
.boxed()
.collect(Collectors.toList());
res.forEach(System.out::println);
5.3.4、flatmap
Java8中的Stream流可以处理通过map方法处理基本类型及(String),但却无法对数组和列表进行操作,flatMap方法弥补了这一不足。flatMap 把 input Stream 中的层级结构扁平化,就是将最底层元素抽出来放到一起。看一个简单的例子:
List<String> list1 = Arrays.asList("a", "b", "c");
List<String> list2 = Arrays.asList("aa", "bb", "cc");
List<String> list3 = Arrays.asList("aaa", "bbb", "ccc");
Stream<List<String>> stream = Stream.of(list1,list2,list3);
List res = stream.flatMap(p->p.stream()).collect(Collectors.toList());
res.forEach(System.out::println);
结果是:
5.3.5、limit
limit方法指定数量的元素的流。对于串行流,这个方法是有效的,这是因为它只需返回前n个元素即可,但是对于有序的并行流,它可能花费相对较长的时间,如果你不在意有序,可以将有序并行流转换为无序的,可以提高性能。
List<String> list = Arrays.asList("a", "b", "c","d");
Stream<String> stream = list.stream();
List res = stream.limit(2).collect(Collectors.toList());
res.forEach(System.out::println);
5.3.6、peek
生成一个包含原Stream所有元素的新Stream,同时提供一个消费函数(Consume),当Stream中每个元素被消费(访问)时都会执行给定的消费函数。
Stream.of("one", "two", "three", "four")
//通过filter获取新Stream ,stream1
.filter(e -> e.length() > 3)
//返回一个包含stream1的Stream stream2,监控stream2中元素的消费
.peek(e -> System.out.println("Filtered value: " + e))
//通过map,获取新的Stream,stream3
.map(String::toUpperCase)
//返回一个包含stream3的Stream stream4,监控stream4中元素的消费
.peek(e -> System.out.println("Mapped value: " + e))
//开始消费(一个元素执行完所有操作之后再对下一个元素进行处理)
//首先消费 stream2中的"three"(map(String::toUpperCase)操作),触发第一个peek,
//然后消费 stream4中的"THREE"(collect操作),触发第二个peek
//接着消费 stream2中的"four"...
.collect(Collectors.toList());
结果:
5.3.7、sorted
sorted()将流中的元素按照自然排序方式进行排序,如果元素没有实现Comparable,则终点操作执行时会抛出java.lang.ClassCastException异常。
sorted(Comparator<? super T> comparator)可以指定排序的方式。
对于有序流,排序是稳定的。对于非有序流,不保证排序稳定。
IntStream intStream = IntStream.of(4, 7, 9, 11, 7, 6, 7, 5321, 342, 5);
intStream.boxed()
.sorted()
.forEach(System.out::println);
5.3.8、skip
skip返回丢弃了前n个元素的流,如果流中的元素小于或者等于n,则返回空的流。
5.4、terminal操作
5.4.1、Match
public boolean allMatch(Predicate<? super T> predicate)
public boolean anyMatch(Predicate<? super T> predicate)
public boolean noneMatch(Predicate<? super T> predicate)
这一组方法用来检查流中的元素是否满足断言。
- allMatch只有在所有的元素都满足断言时才返回true,否则flase,流为空时总是返回true
- anyMatch只有在任意一个元素满足断言时就返回true,否则flase,
- noneMatch只有在所有的元素都不满足断言时才返回true,否则flase,
System.out.println(IntStream.of(1, 2, 3, 4, 5, 6, 7).boxed().anyMatch(n -> n > 3)); //true
System.out.println(IntStream.of(1, 2, 3, 4, 5, 6, 7).boxed().allMatch(n -> n > 3)); //false
System.out.println(IntStream.of(1, 2, 3, 4, 5, 6, 7).boxed().noneMatch(n -> n > 3)); //false
System.out.println(IntStream.of(1, 2, 3, 4, 5, 6, 7).boxed().noneMatch(n -> n > 8)); //true
System.out.println(IntStream.of(1, 2, 3, 4, 5, 6, 7).boxed().allMatch(n -> n > 0)); //true
5.4.2、count
count方法返回流中的元素的数量。它实现为:
mapToLong(e -> 1L).sum();
5.4.3、可变聚合—collect
可变聚合:把输入的元素们累积到一个可变的容器中,比如Collection或者StringBuilder;
第一种方式:现成的收集器
<R,A> R collect(Collector<? super T,A,R> collector)
java8为collect方法提供了工具类Collectors,可以方便地生成List,Set,Map等集合。
Collectors是一个值得关注的类,你需要熟悉这些特定的收集器,如聚合类averagingInt、最大最小值maxBy minBy、计数counting、分组groupingBy、字符串连接joining、分区partitioningBy、汇总summarizingInt、化简reducing、转换toXXX等。
例子:
Stream<String> stream1 = Arrays.asList("a", "b", "c", "a").stream();
Map<String,Long> res =stream1.collect(Collectors.groupingBy((n)->n,Collectors.counting()));
System.out.println("分组情况为:");
res.forEach((k,v)->System.out.println(k+":"+v));
第二种方式:定制收集器
<R> R collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner)
相当于下面的代码:
R result = supplier.get();
for (T element : this stream) accumulator.accept(result, element);
return result;
参数:
- supplier 创建新结果容器的函数。
- accumulator 一个将当元素添加到目标中的方法
- combiner 一个将中间状态的多个任务结果整合到一起的方法(并发的时候会用到)
例子:
Stream<String> stream = Stream.of("a","b","c","d","e");
List<String> list = stream.collect(ArrayList::new,ArrayList::add,ArrayList::addAll);
System.out.println(Arrays.toString(list.toArray())); //abcde
而且在并行stream的collect操作中使用非并发收集器是安全的。这意味着我们下面的代码是没有线程安全的问题:
Stream<String> stream = Stream.of("a","b","c","d","e").parallel();
List<String> list = stream.collect(ArrayList::new,ArrayList::add,ArrayList::addAll);
System.out.println(Arrays.toString(list.toArray())); //abcde
原因是,每个任务都自己维护一个supplier提供的容器,保存当前的中间结果,当中间结果被合并时,它们在任务之间被安全地切换,并且在任何时候只有一个任务合并任何一对中间结果。
任务间结果的合并可以参考下图中的流程:
5.4.4、find
- findAny()
- findFirst()
findAny()返回任意一个元素,如果流为空,返回空的Optional,对于并行流来说,它只需要返回任意一个元素即可,所以性能可能要好于findFirst(),但是有可能多次执行的时候返回的结果不一样。
findFirst()返回第一个元素,如果流为空,返回空的Optional。
5.4.5、forEach、forEachOrdered
- forEach 对于并行流管道,此操作不保证遵守流的遇到顺序,因为这样做将牺牲并行性的好处。
- forEachOrdered 如果流具有定义的顺序,则以流的定义顺序对该流的每个元素执行操作。
5.4.6、max、min
- max返回流中的最大值,
- min返回流中的最小值。
5.4.7、toArray()
将流中的元素放入到一个数组中。
5.4.8、不可变聚合—reduce
除去可变聚合,剩下的,一般都不是通过反复修改某个可变对象,而是通过把前一次的汇聚结果当成下一次的入参,反复如此。比如reduce,count,allMatch;
reduce 操作可以实现从Stream中生成一个值,其生成的值不是随意的,而是根据指定的计算模型。
reduce方法有三个override的方法:
pubic Optional<T> reduce(BinaryOperator<T> accumulator)
pubic T reduce(T identity, BinaryOperator<T> accumulator)
pubic <U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)
我们只看后两个方法:
pubic T reduce(T identity, BinaryOperator accumulator)
相当于:
T result = identity;
for (T element : this stream) result = accumulator.apply(result, element) return result;
参数:
- identity 提供一个值作为计算模型的初始值(一个实例)
- accumulator 计算模型(累积计算器)
这个方法的逻辑是,以identity作为计算模型的初始值,通过累积调用accumulator,最后得出结果。
下面是一个计算总和的例子:
Stream<Integer> stream = IntStream.of(1,3,5,7,9,12).boxed();
Integer res = stream.reduce(0,(sum, b)->sum+b);
System.out.println(res);
(sum, b)->sum+b中,sum代表上次调用计算模型的结果(初始值为identity参数值,也就是0),b代表流中的元素。
pubic < U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator< U> combiner)
按照上面的方法,已经足够使用,那么这个三个参数的方法的作用是什么呢?是为了通过并行计算,提高第二个方法的效率。
参数:
- identity 提供一个值作为计算模型的初始值
- accumulator 计算模型(累积计算器)
- combiner 组合器,将不同任务的计算结果合并
这个方法的逻辑是通过使用多个线程对流中的元素进行计算,最后通过combiner进行组合结果,不过这里发现identity这里有个坑,就是每个子任务使用累积计算器的时候初始值都是identity,这里可能会发生下面的问题(将初始值置为1):
Stream<Integer> stream1 = IntStream.of(1,3,5,7,9,12).boxed();
Integer res1 = stream1.reduce(1,(sum, b)->sum+b);
//38 =(1+3+5+7+9+12)
System.out.println(res1);
Stream<Integer> stream2 = IntStream.of(1,3,5,7,9,12).boxed().parallel();
Integer res2 = stream2.reduce(1,(sum,n)->sum+n,(a,b)->a+b);
//45 = ((1+1)+(1+3)+(1+5)+(1+7)+(1+9)+(1+12))
System.out.println(res2);
上面的代码中,第二个流为并行流,计算的时候,每个任务计算的时候,初始值都是1,这就导致了一个错误的结果。
所以官方文档中对identity的值的限制做了一些说明:
identity值必须是组合器功能的标识。
这意味着,对于所有u(流中的元素),combiner(identity, u)等于u 。
另外combiner功能必须兼容accumulator功能; 对于所有u和t ,以下必须保持:combiner.apply(u, accumulator.apply(identity, t)) == accumulator.apply(u, t)
combiner.apply(u, accumulator.apply(identity, t)) 中u代表并行下其他子任务的结果,t代表流中的一个元素
accumulator.apply(u, t) 中u代表非并行条件下的上一个计算结果,t代表流中的元素。
显然这只是并行和非并行两种不同的处理运算方式,他们应该是相同的。
按照上面的限制,我们把例子中的identity置为0,则一切都正常了。
一些最佳实践
- 流可以从非线程安全的集合中创建,当流的管道执行的时候,非线程安全的数据源不应该被改变。例如下面的代码会抛出异常:
List<String> l = new ArrayList(Arrays.asList("one", "two"));
Stream<String> sl = l.stream();
sl.forEach(s -> l.add("three"));
在设置中间操作的时候,可以更改数据源,只有在执行终点操作的时候,才有可能出现并发问题(抛出异常,或者不期望的结果),比如下面的代码不会抛出异常:
List<String> l = new ArrayList(Arrays.asList("one", "two"));
Stream<String> sl = l.stream();
l.add("three");
sl.forEach(System.out::println);
对于线程安全的数据源,不会有这样的问题,比如下面的代码很正常:
List<String> l = new CopyOnWriteArrayList<>(Arrays.asList("one", "two"));
Stream<String> sl = l.stream();
sl.forEach(s -> l.add("three"));
- 使用无状态的lambda。如果流操作的行为参数是有状态的,流管道结果可能不确定或不正确。 有状态的lambda(或实现适当的功能接口的其他对象)是结果取决于在流管道的执行期间可能改变的任何状态。
例如下面的代码,可能会有问题:
public class StreamTest {
@Test
public void test1() {
final State state = new State();
List<String> l = new ArrayList(Arrays.asList("one", "two","three","four","five"));
l.stream().map((str)->{
if(state.flag){
return "OK";
}else{
state.flag = true;
return str;
}
});
}
}
class State{
boolean flag;
}
- 以函数式编程的思想使用Stream。Stream整个体系设计在函数式编程的思想之上,所以我们使用的时候也应该使用函数式编程的思想来使用Stream,否则很可能出现一些副作用。
简单聊下函数式编程,实在了解的有限,现在结合自己的学习情况简单做下说明:
函数式编程的特点:
1、函数是一等公民,可以当做变量,函数的参数
2、函数式编程中的不变性,用java来解释就是所有的变量都是final的,如果需要改变状态,只能通过函数的调用
例如:
先看一个非函数式的例子:
int cnt;
void increment(){
cnt++;
}
那么,函数式的应该怎么写呢?
int increment(int cnt){
return cnt+1;
}
你可能会觉得这个例子太普通了。是的,这个例子就是函数式编程的准则:不依赖于外部的数据,而且也不改变外部数据的值,而是返回一个新的值给你。
3、无副作用
不会产生side effect,不需要对外部的状态产生顾虑,这样也就会避免了线程安全等一些由于状态改变引起的问题