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

作者: 娃娃要从孩子抓起 | 来源:发表于2017-08-08 17:54 被阅读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都大同小异,大家可以自己去看看,我就不做详细介绍了。

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

    相关文章

      网友评论

        本文标题:JAVA8你只需要知道这些(3)

        本文链接:https://www.haomeiwen.com/subject/smriqxtx.html