java stream api

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/daguanjia11/article/details/78741893

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对象。

像上面的例子一样,filtermapsorted是三个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();

这篇博客写了好久,给个赞如何?

猜你喜欢

转载自blog.csdn.net/daguanjia11/article/details/78741893