Android开发Android技术知识程序员

JAVA8你只需要知道这些(3)

2017-08-08  本文已影响817人  娃娃要从孩子抓起

前言

在上篇文章中,我们提到了java.util.stream包,今天我们就来详细的研究一下这个包。

整体框架

分析stream包,我们先从整体架构入手,然后再深入到细节。我们先来看看API文档:

1.png

  从上图中可以看见stream包中的接口比较多,类和枚举比较少。我们先来看接口:

java8-stream.png

  DoubleStream,IntStream,LongStream,Stream都继承于BaseStream接口。并且它们都有各自的Builder接口:DoubleStream.Builder,IntStream.Builder,LongStream.Builder,Stream.Builder。剩下就只有Collector接口,Collectors,StreamSupport类,Collector,Characteristics枚举。

Stream接口

Stream接口是一个泛型接口,而DoubleStream,IntStream,LongStream只不过是对double,int,long的包装而已,所以我们弄懂Stream,其他的接口也都大同小异。

1.forEach

void forEach(Consumer<? super T> action)

forEach接收一个Consumer接口,该接口我们之前讲Function包时已经提过了。它只接收不参数,没有返回值。然后在 Stream 的每一个元素上执行该表达式。

范例:

Stream<String> stream = Stream.of("I", "love", "you");
        stream.forEach(System.out::println);

System.out.println方法我们都很熟悉了,它接收一个参数,并且在控制台打印出来。这正好符合Consumer接口,所以这里输出的结果是 :

I
love
you

2.peek

Stream<T> peek(Consumer<? super T> action)

peek方法也是接收一个Consumer功能型接口,它与forEach的区别就是它会返回Stream接口,也就是说forEach是一个Terminal操作,而peek是一个Intermediate操作,forEach完了以后Stream就消费完了,不能继续再使用,而peek还可以继续使用。
范例:

Stream<String> stream = Stream.of("I", "love", "you");
        stream.peek(System.out::println).forEach(System.out::println);

代码很简单,但是大家可以先思考一下,输出的结果是什么?
输出结果:

I
I
love
love
you
you

怎么样?跟你想的是一样的吗?有人可能会问,为什么输出结果不是以下这种呢?

I
love 
you
I
love
you

明明peek方法在前面。这是因为我们前面提到过的懒加载,peek是一个Intermediate操作,它并不会马上执行,当forEach的时候才会把peek和forEach一起执行,来提高效率,所以等于是每个stream元素执行两次打印操作,再执行下一个元素。

3.filter

Stream<T> filter(Predicate<? super T> predicate)

filter方法接收一个断言型的接口,断言型接口接收一个参数,返回一个Boolean类型。filter方法根据某个条件对stream元素进行过滤,通过过滤的元素将生成一个新的stream。
范例:

Stream<Integer> stream = Stream.of(1, 2, 3,4,5,6);
        stream.filter((n)->n>2).forEach(System.out::println);

以上代码通过filter方法把大于2的元素过滤出来,然后输出。

4.map

<R> Stream<R> map(Function<? super T,? extends R> mapper)

map方法接收一个功能型接口,功能型接口接收一个参数,返回一个值。map方法的用途是将旧数据转换后变为新数据,是一种1:1的映射,每个输入元素按照规则转换成另一个元素。该方法是Intermediate操作。

Stream<String> stream = Stream.of("a","b","c","d");
        stream.map(String::toUpperCase).forEach(System.out::println);

以上代码通过map方法,把a,b,c,d全部转变成大写,然后输出。

5.flatMap

<R> Stream<R> flatMap(Function<? super T,? extends Stream<? extends R>> mapper)

flatMap从结构上来看跟map差不多,主要是可以用来将stream层级扁平化。

Stream<List<Integer>> inputStream = Stream.of(
                 Arrays.asList(1),
                 Arrays.asList(2, 3),
                 Arrays.asList(4, 5, 6)
                 );
        inputStream.flatMap((n)->n.stream()).forEach(System.out::println);

我们可以看见,inputStream由3个list组成,在经过flatMap以后,list就没有了,以前list中的元素全部放在了一起。相关的方法还有:flatMapToInt,flatMapToLong,flatMapToDouble,只不过他们返回的分别是IntStream,LongStrea和DoubleStream。

6.findFirst:返回stream的第一个元素的Optional或为空。这是一个Terminal操作,也是一个短路操作。

7.count:返回此流元素的数量。

8.sorted:将此流中的元素根据自然顺序排序,sorted方法还有一个重载方法,可以传入一个Comparator,这样就可以根据Comparator来排序。

9.min/max:Stream接口中的这两个方法接收一个Comparator参数,通过Comparator返回此流最小或者最大的元素。IntStream,DoubleStream.LongStream则不需要传入Comparator。

10.limit:该方法接收一个long型参数,表示一共返回几个元素。

11.skip:接收一个long类型的参数,表示跳过几个元素。

12.distinct:消除重复元素后返回一个新Stream。

13.allMatch:Stream中的所有元素满足传入的断言型接口,就返回true。

14.anyMatch:Stream中的只要有一个元素满足传入的断言型接口,就返回true。

15.noneMatch:Stream中没有元素满足传入的断言型接口,就返回true。

16.generate:接收一个Supplier接口,返回一个Stream,通过实现supplier接口,可以自己来控制流的生成。

17.iterate:

static <T> Stream<T> iterate(T seed, UnaryOperator<T> f)

iterate接收两个参数,第一个是泛型,seed可以理解为种子值或者起始值。UnaryOperator是一个接口:

@FunctionalInterface
public interface UnaryOperator<T> extends Function<T,T>

该接口继承了Function接口,那么也必须实现Function接口中的apply方法。除此之外该接口还有一个静态方法---identity,该方法始终返回其输入参数。
iterate方法的作用是将种子值成为stream的第一个元素,f(seed)为第二个元素,f(f(seed))为第三个元素,说递归你应该比较容易明白。

范例:

Stream.iterate(3, n->n+3).limit(10).forEach(System.out::println);

输出:

3
6
9
12
15

以上范例中,3即为种子值,然后f(3)等于6,f(f(3))得9。需要注意的是,iterate方法和generate方法返回的都是无限stream,需要用limite来限制stream的长度。

18.reduce:
reduce提供了三种重载方法。

1. Optional<T>  reduce(BinaryOperator<T> accumulator)
2. T    reduce(T identity, BinaryOperator<T> accumulator)
3. <U> U    reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)

第一个方法返回一个Optional对象,接收一个BinaryOperator。我们先来看BinaryOperator是什么?

@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T,T,T>

可以看到BinaryOperator是一个函数型接口,继承了BiFunction,并且传入参数和返回值都是相同类型。我们接着看BiFunction的定义:

@FunctionalInterface
public interface BiFunction<T, U, R>{
  R apply(T t, U u);
}

BiFunction接口中有一个apply方法,有两个参数,一个返回值。到这里我们大概知道reduce方法传入的参数大概怎么用了,在来看返回值Optional的定义:

public final class Optional<T> extends Object

Optional是一个普通的对象,里面的方法大家可以自己去看API,这里就不详细说了。到这里你可能会说,写了这么多,你也没说reduce到底有什么作用啊?我们通过名字去猜测一下,reduce有减少,归纳之意。那我们是否可以理解为,把Stream提供的多个元素归纳成一个对象?

范例:

Integer sum=Stream.of(1,2,3,4).reduce(Integer::sum).get();
        System.out.println(sum);

输出:

10

我们通过reduce方法,把1-4累加起来得到结果10.你肯定会问为什么传的参数是Integer::sum?我们在以前的文章里面提到了方法引用::,在这里就是引用了Integer类的sum方法:

static int sum(int a, int b)  

这个方法是不是就跟BiFunction中定义的apply一样呢?接收两个参数和一个返回值。
我们接着看reduce的第二个重载方法,在这个重载方法中多了一个参数T,这就是起始值,然后返回值由Optional变成了T。

范例:

Integer sum=Stream.of(1,2,3,4).reduce(1,Integer::sum);
        System.out.println(sum);

输出:

11

在此范例中,我们添加了起始值1,使得最后输出结果多加了1。如果你觉得还不明白,那么再来看一个例子。

范例:

String sum=Stream.of("a","b","c","d").reduce("1",String::concat);
        System.out.println(sum);

输出:

1abcd

这会应该明白了。
  关于reduce的第三个重载方法,主要是用于parallelStream的,reduce操作是并发进行的,为了避免竞争,每个reduce线程都会有独立的result,combiner参数的作用就是在于合并每个线程的result得到最终的结果。由于第三个方法不是特别常用,我就只说一下方法不给出范例了。

<U> U    reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)

这个方法起初一看,头都大了,这都是什么鬼?又是U,又是T,又是BiFunction,又是BinaryOperator。BinaryOperator不是继承与BiFunction的么?为什么不两个都使用BiFunction呢?

那我们就来解析一下这个方法,首先该方法的返回值是由第一个参数决定的。也就是说第一个参数是什么类型,该方法就返回什么类型。这点明确了很重要。

我们接着看第二个参数-BiFunction,为了理解深刻,我们再次拿出该接口的定义:

@FunctionalInterface
public interface BiFunction<T, U, R>{
  R apply(T t, U u);
}

该接口接收两个参数,这两个参数的类型可以不一致。并且返回一个值,值的类型也可以不一致。
接着我们看reduce方法里面定义的

BiFunction<U,? super T,U> accumulator

我们来对应一下,该接口接收两个参数,其中第一个为U,第二个为T的子类,返回类型为U。这下就明白多了,也就是说接收两个不同类型的参数,但是返回值类型跟第一个参数一致,而第一个参数的类型也就是reduce方法的第一个参数类型U。

在看reduce第三个参数-BinaryOperator

@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T,T,T>

该接口继承了BiFunction,但是最重要的是,继承的BiFunction的两个接收参数和返回值都是同一个类型T。所以简单来说BinaryOperator接收两个参数,返回一个值都是同一类型。

到这里我们应该明白了为什么reduce第二个参数是BiFunction,第三个参数是BinaryOperator了吧?
因为第二个参数的作用是accumulator,所以接收的两个参数类型可以不一样。而前面说了在parallelStream的情况下,combiner的作用是合并每个线程的结果,而每个线程返回的结果都应该是同一个类型,所以在这里用BinaryOperator而不是BiFunction。

不得不说这种设计真的是太精妙了。

19.collect:
  collect方法跟reduce方法功能很类似,都是聚合方法。不同的是,reduce方法在操作每一个元素时总创建一个新值,而collect方法只是修改现存的值,而不是创建一个新值。

方法定义:

1.<R,A> R collect(Collector<? super T,A,R> collector)  
2.<R> R collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner)  

这两个方法都是泛型方法,我们先看第一个。第一个方法接收一个Collector接口作为参数。如果我们要自己实现它会很麻烦,好在java.util.stream包中给我们提供了一个叫Collectors的类。这个方法我就不在这里介绍了,大家可以自己去看API,通过Collectors这个类我们可以很容易得到一个Collector对象,这个类中提供了很多统计的操作和创建集合的操作。

范例:

Stream<String> stream = Stream.of("a", "b", "c", "d");
List<String> list =stream.collect(Collectors.toList());
        for (String string : list) {
            System.out.println(string);
        }

输出:

a
b
c
d

在这里我们将一个stream流转换为了一个List对象。

collect方法的第二种形式跟我们前面说的reduce的很像。接收3个参数,第一个参数是Supplier接口,这个接口我们以前说过。

@FunctionalInterface
public interface Supplier<T> {

    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}

BiConsumer接口跟Consumer接口类似,不同的是Consumer接口只接收一个参数而BiConsumer接口接收两个参数。collect的第二个参数和第三个参数都是BiConsumer接口,但是参数类型却不一样。BiConsumer<R,? super T> 第一个参数跟collect返回值一样,也跟第一个参数一样。第二个参数类型跟stream的类型一样。BiConsumer<R,R>则两个参数类型是相同的。

范例:

System.out.println(Arrays.asList("1","2","3","4").parallelStream().collect(
                StringBuilder::new,
                new BiConsumer<StringBuilder,String>(){

                    @Override
                    public void accept(StringBuilder t, String u) {
                        System.out.println("accumulator operate current thread:"+Thread.currentThread().getId()+"   t:"+t+" u:"+u);
                        t.append(u);
                        System.out.println("accumulator operate current thread:"+Thread.currentThread().getId()+"   result t:"+t+" u:"+u);
                    }
                    
                }
        , new BiConsumer<StringBuilder,StringBuilder>(){

            @Override
            public void accept(StringBuilder t, StringBuilder u) {
                System.out.println("combiner operate current thread:"+Thread.currentThread().getId()+"   t:"+t+" u:"+u);
                t.append(u);
                System.out.println("combiner operate current thread:"+Thread.currentThread().getId()+"   result t:"+t+" u:"+u);
            }
                            
                        }));

输出:

accumulator operate current thread:1   t: u:3
accumulator operate current thread:11   t: u:4
accumulator operate current thread:10   t: u:2
accumulator operate current thread:12   t: u:1
accumulator operate current thread:12   result t:1 u:1
accumulator operate current thread:10   result t:2 u:2
accumulator operate current thread:11   result t:4 u:4
accumulator operate current thread:1   result t:3 u:3
combiner operate current thread:1   t:3 u:4
combiner operate current thread:10   t:1 u:2
combiner operate current thread:1   result t:34 u:4
combiner operate current thread:10   result t:12 u:2
combiner operate current thread:10   t:12 u:34
combiner operate current thread:10   result t:1234 u:34
1234

为了方便大家理解,我并没有使用lambda表达式。我们首先创建了一个并行的stream,每个stream元素的类型为String,接着我们调用了collect方法,collect方法第一个参数是创建一个StringBuilder对象,在第二个参数中,我们打印了当前的线程id,和t,u的值方便调试。执行的操作也只是把String加入到StringBuilder中,第三个参数则把两个StringBuilder合并。从输出结果中我们可以看见,在执行accumulator操作的时候t的值是空的,并且是4个线程同时进行了accumulator操作,每个线程都把String加入到了StringBuilder中,而在执行combiner操作的时候,就由4个线程变成了2个,然后进行合并操作。最终结果为1234。由于是多线程的,所以每次输出的顺序是不一样的。以上输出只能作为参考。

到目前为止,Stream接口中的大部分方法我们都讲过了。至于那些IntStream,LongStream都大同小异,大家可以自己去看看,我就不做详细介绍了。

如果你觉得本篇文章帮助到了你,希望大爷能够给瓶买水钱。
本文为原创文章,转载请注明出处!

上一篇下一篇

猜你喜欢

热点阅读