[Java]“语法糖”系列(三)之集合流操作(Stream)[笔记]

来源:互联网 发布:中软国际java培训骗局 编辑:程序博客网 时间:2024/05/16 08:23

>集合流操作

    这里的这个流和IO流不一样,是JDK8中对Collection对象功能的加强。

    个人认为作为基础,IBM上的>这篇文章<写得已经很好了,已然再无他文可出其右。这里只做一个要点笔记。


>流操作

    流操作分为两类:

  • Intermediate(转换操作):一个流可以后面跟随零个或多个 intermediate 操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。
  • Terminal(终点操作):一个流只能有一个 terminal 操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果,或者一个 side effect。

    所有的转换操作都是lazy的,lazy是指多次转换操作只会在遇到终点操作之后,才会依次执行。在终点操作之后,流截止,不再允许更多操作。比如:

List<Integer> nums = Arrays.asList(1, 2, 3);nums.stream().peek(System.out::println).map(x->x*-1).forEach(System.out::println);
    输出为: 1 -1 2 -2 3 -3。


>流操作分类

  • 【数据集合】 ->【数据源】 - >  若干次(包括0)【转换操作】 ->【终点操作】。

     >这篇文章<把常用操作都用图表示出来了,值得记录一下。


     转换操作:map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel...

     终点操作:forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny...

     其实还有一种短路操作,下面再说。


>终点操作:Collect方法

    看下面这段代码:

Set<String> result = Stream.of("aa", "bb", "cc", "aa").collect(() -> new HashSet<String>(),(set, item) -> set.add(item),(set, subSet) -> set.addAll(subSet));
   其中collect方法源码为:

<R> R collect(Supplier<R> supplier,              BiConsumer<R, ? super T> accumulator,              BiConsumer<R, R> combiner);
    Supplier是一个函数式接口:

@FunctionalInterfacepublic interface Supplier<T> {    /**     * Gets a result.     *     * @return a result     */    T get();}
    其功能为直接返回一个传入的实例。() -> new HashSet<String>()等价于:

new Supplier<Set<String>>() {@Overridepublic Set<String> get() {return new HashSet<>();}}
    BiConsumer也是一个函数式接口,接受两个传入的参数,无返回值。(set, item) -> set.add(item)等价于:

new BiConsumer<Set<String>, String>() {@Overridepublic void accept(Set<String> set, String item) {set.add(item);}}
    同理(set, subSet) -> set.addAll(subSet))等价于:
new BiConsumer<Set<String>, Set<String>>() {@Overridepublic void accept(Set<String> set, Set<String> subSet) {set.addAll(subSet);}}
   collect方法在ReferencePipeLine.java -> ReduceOps.java中被实现:
public static <T, R> TerminalOp<T, R>    makeRef(Supplier<R> seedFactory,            BiConsumer<R, ? super T> accumulator,            BiConsumer<R,R> reducer) {        Objects.requireNonNull(seedFactory);        Objects.requireNonNull(accumulator);        Objects.requireNonNull(reducer);        class ReducingSink extends Box<R>                implements AccumulatingSink<T, R, ReducingSink> {            @Override            public void begin(long size) {                state = seedFactory.get();            }            @Override            public void accept(T t) {                accumulator.accept(state, t);            }            @Override            public void combine(ReducingSink other) {                reducer.accept(state, other.state);            }        }        return new ReduceOp<T, R, ReducingSink>(StreamShape.REFERENCE) {            @Override            public ReducingSink makeSink() {                return new ReducingSink();            }         };    }

     最终由StreamPipe送入每一个数据,通过累加器 accumulator 叠加到集合中。

     <R> R collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner);接受的三个函数参数都有具体的要求,第一个参数要求返回一个产生数据容器result的实例;第二个函数参数要求是一个无状态、无打断(异常)的累加函数,用于把数据流中的每个数据装入result容器中;第三个函数参数要求传入一个与累加函数兼容的结合函数,用于叠加并行流的多个数据容器。

    在经过方法引用简写后,上文的例子可简化为:

Set<String> result = Stream.of("aa", "bb", "cc", "aa").collect(HashSet::new,HashSet::add,HashSet::addAll);
    事实上对于Java提供的集合对象,已有封装好的方法:

Set<String> result = Stream.of("aa", "bb", "cc", "aa").collect(Collectors.toSet());



>函数接口
     上面那个例子里提到了函数接口,我找到了一张描述常用函数接口的图:



>短路操作Short-circuiting()

     其实不难发现,所有的转换操作最终返回的都是一个流,而终点操作不再返回一个流,而是直接触发了Pipe中取出数据的操作,给所有激活所有沿途lazy的转换操作。

     短路操作其实和终点操作也是一样的,可能不再返回一个流,或是返回一个被截取过的流。比如anyMatch方法,通过Predicate<T>接口返回了一个真值。

     由于流Stream在理论上是无限大的,短路操作被用以对流进行截取,把无限的变成有限的流,比如limit方法,可以限制获取数据源的数量。


     短路操作:anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 limit...


     下面这个例子是就是一个短路操作,用于检测数据流中是否含有"aa"字符串。相比于迭代的方式,这钟写法更偏向自然语言。

boolean string = Stream.of("aa", "bb", "cc", "dd").anyMatch("aa"::equals);System.out.println(string);
     再举一个关于短路操作的例子。findFirst方法用于获取数据流中的第一个数据,返回一个Optional对象。Optional对象是Java仿Scala语言设计的一种可空对象,在>这篇文章<里有详尽解释。
Stream.of("aa", "bb", "cc", "dd").findFirst().ifPresent(System.out::println);// 打印 aa


>海纳百川的流:flatMap

    Stream是无限的,并且流也接受不同的数据类型。这就带来一个问题,Steam是Collection类的功能加强,但Collcetion显然不能一次性接受所有的数据类型。

    Steam中提供了一个接口方法flatMap,用于把不同的数据集拍平,无论你是List还是Set,统一还原成数据源Source。

    例如我现在有个学校,学校下有若干个班级,班级里有若干个学生。我要把这个学校直接拍平并转化成学生的数据流:

List<List> school = new LinkedList<>();List<String> class1 = new LinkedList<>();List<String> class2 = new LinkedList<>();class1.add("小明");class1.add("小王");class2.add("小李");school.add(class1);school.add(class2);school.stream().flatMap(e->e.stream()).forEach(System.out::println);// 打印: 小明 小王 小李

如果需要拍平二层以上的流,如现在有一个List<List>region.add(school),对于拍平、并获取整个地区所有学校所有班级的学生名字的流,需要做一个转型,这么写:

region.stream().flatMap(Collection::stream)// 用了"方法引用"的语法糖,详见:http://blog.csdn.net/shenpibaipao/article/details/78614280.flatMap(e->((List<String>) e).stream())// 转型成Collection形式.forEach(System.out::println);