stream api是jdk1.8中引入的,位于java.util.stream
中,它基于lambda表达式,扩展了集合操作的能力。
stream api比较类似apache spark的api,以及.net框架中的Linq。类似这种的计算能力一般都是在一个数据集合中进行的,尽管在java中没有集成到Collection框架中,但是在其它语言中,这种操作集合或数组紧密集成的,如,python和javascipt中的数组,spark中的rdd,以及c#中的集合框架。
stream api还提供了并行计算的能力。
下面来讨论一下如何使用java中的stream api。关于java中如何使用lambda表达式,可以参考java中的lambda表达式
如何获得一个stream
java.util.stream
包下定义了一个BaseStream
类,使用最多的是它的Stream
子类。它的定义如下:
public interface Stream<T> extends BaseStream<T, Stream<T>>
由于BaseStream是泛型类,泛型参数不支持原始类型,java还专门针对几个原始类型提供了对应的子类,包括
- IntStream
- LongStream
- DoubleStream
它们几个的使用方法基本上与Stream类类似,所以我在本片博客中只使用Stream类。
java提供了以下几种方式来获得一个Stream对象
- 从Collection中获取
- 从Array中获取
- 从已有的Stream对象中获取
从Collection中获得
List<Integer> list = Arrays.asList(1, 2, 3, 6, 5, 10, 7, 8, 9, 4);
Stream<Integer> stream = list.stream();
这个stream()
是定义在java.util.Collection<E>
接口中的一个default方法,所以,所有实现了这个接口的类都可以直接使用,也就意味着,所有的集合类都可以转换为Stream。
从Array中获得
String array[] = {"hello", "java", "stream", "api"};
Stream<String> stream1 = Arrays.stream(array);
Arrays
类提供了一个静态方法来将一个数组转换为Stream并返回。
从已有的Stream对象中获取
Stream
接口中定义了一些intermediate类型的方法,如map,filter,可以将一个已存在的Stream对象中转换为另一个Stream对象。后面会提到,此处先略过。
一个简单的例子
进一步讨论stream api之前,先来看一个简单的例子
Stream<Integer> stream = Arrays.asList(1, 2, 3, 6, 5, 10, 7, 8, 9, 4).stream();
stream.filter(item -> item % 2 == 0).map(item -> item * 10).sorted().forEach(item -> System.out.println(item));
第一行代码从一个List中创建了一个Stream对象。第二行代码显示筛选所有的偶数,然后将每个数字乘以10,最后把他们打印出来,运行结果是
20
40
60
80
100
可以看出来,stream api加上lambda表达式,使java代码变的非常的简洁。
intermediate方法和terminal方法
就像spark的api分为transformation和action两类一样,java的stream api也分为intermediate方法和terminal方法。理解这两者的区别是比较有必要的。
一般来讲,intermediate方法用来设置转换规则,terminal方法用来计算最终的结果。intermediate方法并不马上进行实际的计算,直到遇到一个terminal方法为止。intermediate是惰性计算的,这么设计的目的是为了提高性能。intermediate方法生成一个临时的Stream对象,这个Stream对象可以继续调用别的intermediate方法或terminal方法,一旦调用了terminal方法,这个Stream对象就不能再使用了,否则会抛出异常。intermediate方法的返回值还是Stream对象。
像上面的例子一样,filter
,map
和sorted
是三个intermediate方法,forEach
则是terminal方法,在调用forEach
之前,这个方法链可以调用任意多次intermediate方法。
常用的intermediate方法
常用的intermediate方法也就上面列举的那三个,下面具体来看下
filter
filter方法的定义如下
Stream<T> filter(Predicate<? super T> predicate);
泛型参数T表示元素的类型。它接收一个Predicate
对象,Predicate
是一个内置的functional interface,它包含一个boolean test(T t)
抽象方法。当前stream中的每一个数据项都会调用这个predicate中的test方法(通常以lambda表达式的形式提供),所有返回true的项会组成一个新的Stream返回,返回false的项会被直接忽略调。
在上面的例子中,filter是这么使用的
stream.filter(item -> item % 2 == 0)
它的作用就是在当前stream中筛选出所有的偶数,组成一个全新的Stream对象并返回。
filter方法不改变Stream的泛型参数(即元素类型),只减少Stream的元素个数。
map
map方法的定义如下:
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
泛型参数T是元素类型。R是生成的新Stream对象的元素类型。它的参数类型Function
是java内置的一个functional interface,其中包含一个抽象方法R apply(T t)
,这个方法接受一个T类型的参数,返回一个R类型的结果。
调用map方法的Stream对象的每个元素都会调用mapper中定义的apply方法(一般以lambda表达式的形式提供),将该方法的返回值(类型为R)组成一个新的Stream<R>
类型的Stream对象返回。
map方法不改变元素的个数,只改变元素本身,也可以改变元素类型。
在上面的例子中,stream.map(item -> item * 10)
将stream对象中的每个元素乘以10返回。
sorted
sorted方法有两个重载版本,分别是
Stream<T> sorted();
Stream<T> sorted(Comparator<? super T> comparator);
sorted方法会将当前stream对象的元素进行排序,第一个重载版本使用默认的排序方式,第二个重载版本可以通过提供一个comparator来定义排序规则。
在上面的例子中,stream.sorted()
对当前stream对象中的元素重新排序,并返回一个排序好的新对象。
再次强调一次,所有的intermediate方法都不改变当前stream对象,而是生成一个新的stream对象
常用的terminal方法
terminal方法会触发当前的stream对象进行计算,计算之后,当前的stream对象就被终结了。再次引用被终结的stream的话会引发异常。
count
count方法返回当前stream对象中元素的个数
min和max
这两个方法返回当前stream对象的最小值/最大值,定义如下:
Optional<T> min(Comparator<? super T> comparator);
Optional<T> max(Comparator<? super T> comparator);
下面看两个简单的例子
List<Integer> list = Arrays.asList(1, 2, 3, 6, 5, 10, 7, 8, 9, 4);
Optional<?> minNumber = list.stream().min(Integer::compare);
if (minNumber.isPresent()) {
System.out.println("min number is " + minNumber.get());
}
这个例子中,使用Integer::compare
作为比较器,返回一个最小值,然后打印出来。这个语法叫做方法引用。最小值的类型是Optional<Integer>
。上面的代码中也包含了Optional的常见用法。
再来看一个使用自定义比较器的例子。
String array[] = {"hello", "java", "stream", "api"};
Optional<?> mostLongString = Arrays.stream(array).max((s1, s2) -> s1.length() > s2.length() ? 1 : -1);
if (mostLongString.isPresent()) {
System.out.println(mostLongString.get() + " has most long length");
}
上面的例子中,自定义了一个比较规则,从而返回字符串长度最长的一个元素。
forEach
forEach方法比较简单,常用来遍历stream中的每一个元素。最上面的例子中是这么使用forEach的
stream.forEach(item -> System.out.println(item));
reduce
reduce的机制比较类似hadoop中的map-reduce中的reduce过程。
前面没有提到的是,stream有两种形式,sequential stream(连续的流)和parallel stream(并行的流),Collection.stream()
方法返回的是连续的流,而Collection.parallelStream()
方法返回的是并行的流。
reduce方法有三种重载
Optional<T> reduce(BinaryOperator<T> accumulator);
T reduce(T identity, BinaryOperator<T> accumulator);
<U> U reduce(U identity,BiFunction<U, ? super T, U> accumulator,BinaryOperator<U> combiner);
这个有点复杂,先看一下第三个重载版本中的那个functional interface。
public interface BiFunction<T, U, R> {
R apply(T t, U u);
}
然后是前两个重载版本的functional interface
public interface BinaryOperator<T> extends BiFunction<T,T,T>
BiFunction
包含三个泛型参数,T和U是两个参数类型,R是返回值类型,它的意思就是,接收两个任意类型的参数,返回一个任意类型的值。
BinaryOperator
扩展了BiFunction
,本身没有定义新的抽象方法,只是把三个泛型参数改成了一个,也就意味着,它继承了apply方法,但apply方法的两个参数以及一个返回值的类型都必须是一个类型。
sequential stream的reduce执行过程
接下来以sequential stream为例,简单介绍一下reduce的执行过程。
reduce的第一个重载版本接受一个BinaryOperator
类型的lambda表达式,因为一个stream的泛型参数肯定是一样的。当一个stream对象调用第一个重载版本的reduce方法时,会先取stream中的前两个元素来调用BinaryOperator
类型的lambda表达式,将它的返回结果作为下次调用的第一个参数,再取stream中的第三个元素作为第二个参数继续调用BinaryOperator
类型的lambda表达式,将它的返回结果作为下次调用的第一个参数,再取stream中的第四个元素作为第二个参数再次调用BinaryOperator
类型的lambda表达式……,直到stream中最后一个元素被作为第二个参数使用并返回结果,最后的结果将作为reduce的结果返回。
用一个例子说明一下
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
Optional<Integer> sum = list.stream().reduce((a, b) -> {
String msg = MessageFormat.format("a = {0},b = {1}", a, b);
System.out.println(msg);
return a + b;
});
if (sum.isPresent()) {
System.out.println("sum is " + sum.get());
}
上面代码的输出结果是
a = 1,b = 2
a = 3,b = 3
a = 6,b = 4
a = 10,b = 5
a = 15,b = 6
a = 21,b = 7
a = 28,b = 8
a = 36,b = 9
a = 45,b = 10
sum is 55
它的执行过程也很容易看出来:首先取stream的前两个元素,这里是1和2,调用lambda表达式后返回3,然后用这个返回值(3)和第三个元素(3)作为参数继续调用lambda表达式,再用返回值(6)和第四个元素(4)作为参数继续调用lambda表达式,直到最后把所有的元素计算一遍为止。
reduce方法的第二个重载多了一个参数,它的执行过程与第一个重载版本非常相似,除了一点:第一次调用lambda表达式时,identity参数作为lambda表达式的第一个参数,stream中的第一个元素作为lambda表达式的第二个参数,接下来的过程就一样了。
可以这么理解,lambda表达式的两个参数中,第一个参数是上次计算的结果,第二个参数是stream中的下一个元素。由于第一次调用lambda表达式时没有上次的计算结果,所以,第一个重载版本是把stream中的第一个元素作为上次的计算结果,这样的话,此时的下一个元素就是第二个元素。而第二个重载版本则是把identity参数作为上次的计算结果,这样的话,此时的下一个元素就是第一个元素。
parallel stream的reduce执行过程
第三个重载版本看上去这么牛逼,因为它是专门针对parallel stream的。第三个参数combiner是用来合并多个accumulator的结果的。sequential stream执行reduce是不需要combine的,因为这个过程是串行的。
当一个parallel stream调用reduce方法时,stream中的元素并不是从头到尾依次被计算的,而是根据stream的大小动态地分配给了多个线程来同时计算。我们把上面的例子升级成一个parallel stream版的reduce,并调用第三个重载方法,看一下执行的过程
int num = list.parallelStream().reduce(0,
(a, b) -> {
String msg = MessageFormat.format("accumulator:a = {0},b = {1},", a, b);
System.out.println(msg + ". thread name = " + Thread.currentThread().getName());
return a + b;
},
(a, b) -> {
String msg = MessageFormat.format("combiner:a = {0},b = {1}", a, b);
System.out.println(msg + ". thread name = " + Thread.currentThread().getName());
return a + b;
});
System.out.println("num3 is " + num);
除了使用list.parallelStream()
获取parallel stream之外,还打印了当前线程的名字,以及执行阶段(accumulator/combiner)。它的输出结果是
accumulator:a = 0,b = 8,. thread name = ForkJoinPool.commonPool-worker-1
accumulator:a = 0,b = 7,. thread name = main
accumulator:a = 0,b = 9,. thread name = ForkJoinPool.commonPool-worker-3
accumulator:a = 0,b = 3,. thread name = ForkJoinPool.commonPool-worker-2
accumulator:a = 0,b = 2,. thread name = ForkJoinPool.commonPool-worker-1
accumulator:a = 0,b = 10,. thread name = ForkJoinPool.commonPool-worker-3
accumulator:a = 0,b = 5,. thread name = ForkJoinPool.commonPool-worker-2
accumulator:a = 0,b = 1,. thread name = ForkJoinPool.commonPool-worker-1
combiner:a = 9,b = 10. thread name = ForkJoinPool.commonPool-worker-3
accumulator:a = 0,b = 4,. thread name = ForkJoinPool.commonPool-worker-2
combiner:a = 1,b = 2. thread name = ForkJoinPool.commonPool-worker-1
combiner:a = 8,b = 19. thread name = ForkJoinPool.commonPool-worker-3
accumulator:a = 0,b = 6,. thread name = main
combiner:a = 4,b = 5. thread name = ForkJoinPool.commonPool-worker-2
combiner:a = 6,b = 7. thread name = main
combiner:a = 3,b = 9. thread name = ForkJoinPool.commonPool-worker-2
combiner:a = 13,b = 27. thread name = main
combiner:a = 3,b = 12. thread name = ForkJoinPool.commonPool-worker-2
combiner:a = 15,b = 40. thread name = ForkJoinPool.commonPool-worker-2
num is 55
我执行了好几次,每次输出的结果虽然略有差异,但每次都有4个线程参与了计算,打印的行数一样,并且每次的最终结果都是一样的。
通过观察上面的输出结果可以看出,每次执行accumulator的第一个参数都是0,也就意味着,第一个参数identity
与stream中的每一个元素都会执行一次accumulator过程(由第二个lambda表达式指定),共进行了10次accumulator,输出了10个结果。这10个结果又像sequential stream的reduce过程那样执行了一遍combiner,共执行了9次combiner。最后的一次combiner返回了最后的结果55。
由于parallel stream的reduce过程比较复杂,为了保证最终结果的确定性,accumulator过程要满足三个条件:
- 无状态,每个元素都是被单独处理的,相互之间不依赖、不引用
- 不修改stream对象,不对stream对象重新赋值
- 参数无先后之分,计算无先后之分。例如:1+2和2+1返回的结果一样,先计算1+2再计算5+6,与先计算5+6后计算1+2,最终的结果不受影响。
collect
既然可以从Collection对象创建Stream对象,肯定也要有一种机制能够从Stream对象变回Collection对象,collect方法就是用来做这个的。java内置了两种collect方案,下面的两个例子分别将一个stream转变为List和Map
List<?> backList = stream.collect(Collectors.toList());
Set<?> backSet = stream.collect(Collectors.toSet());
collect方法的参数很复杂,但Collectors.toList()
和Collectors.toSet()
的两个方法使用起来既简单又强大,能满足大多数应用场景。Collectors
的全名是java.util.stream.Collectors
toArray和iterator
除了collect()
方法之外,还可以通过toArray()
方法将stream对象转换为数组,只不过返回值是Object[]
类型,而不是泛型。
还可以通过iterator()
方法返回一个Iterator<?>
的可枚举对象。例如:
Iterator<?> iterator = stream.iterator();
这篇博客写了好久,给个赞如何?