JAVA基础之六-Stream(流)简介

我在别的篇幅已经说过:流这个东西偶尔可以用用,但我个人最大的学习动力(目前)仅仅是为了有助于阅读spring越发繁复的源码

 

本文主要介绍Stream接口(包括主要的方法)和相关的几个主要接口:Predicate、ConsumerSupplier

还有Collector接口,Collectors工具类。

由于网上已经有太多的文章介绍,所以,本文侧重点在于简单介绍基本的概念,并不会罗列所有的接口成员的说明(这些可以看javaDoc或者

官方的javaDoc)。

 

虽然主要说的是流,但也顺便说了一些其它东西:函数式接口之类

 

注:本文所相关代码和图是在J17环境下获得的。

一、重要接口、类及其定义

1.1、流直接相关接口/类

仅介绍部分:

  1. Stream
  2. Predicate
  3. Consumer
  4. Supplier
  5. xxxOperator
  6. xxxFunction
  7. Collector
  8. Collectors

Stream,Collector,Collectors是java.util.stream之下的,剩下的都是在java.util.function之下

 

1.Stream(普通接口)

流接口的类层次结构如下图:

可以看到Stream继承自BaseStream,后者则继承了AutoCloseable。顺便提一句,并不是什么都可以自动关闭,如果是文件流需要自己关闭,Stream的javaDoc有提到。

此外,有可以看到,还有几个继承自BaseStream的其它Stream,包括DoubleStream,IntStream,LongStream,..

有点不太明白的是,为什么java17中没有提供其它可以聚集的数据类的Stream,例如为什么没有BigDecimalStream,BigIntStream? 有空再补充。

 

BaseStream重要定义了和计算不太相干的一些行为(并发、迭代等),如下图:

 

Stream自己则专注于计算/转换有关的,如下图:

如上图,都是诸如min,max,count,distinct之类的熟悉字眼,还有份额不小的各种mapXXX。

基本上,需要流做的,或者善于让流做的事情,都定义好了。

 

2.Predicate(函数式接口)

Predicate ,中文有许多翻译:谓词、断言、表明、预测、断定等。

不过结合大量的方法定义,我觉得翻译为“断定”可能更好理解,一个根据输入确定是否真伪的对象,好似人类的推官、断事官、审判员。

为了便于行文,后面都会写成Predicate或者断定

这个断定接口定义了几个重要的方法:

可以看到,只有一个test是需要实现的,其它几个都是默认的或者是静态的。

这个接口要实现类返回一个布尔值,断定传入的参数/内容是否合乎某个标准。

 

3.Consumer(函数式接口)

Consumer-消费者,这个翻译没有异议。

所以消费者接口比较简单,就两个方法:

其中的accept不返回任何东西,重要的是andThen,可以让人一直andThen下去,只要没有异常。

也就是说,消费者可以一直消费,只要愿意。

 

4.Supplier(函数式接口)

和消费者对应的是供应者

和消费者不同的是,供应者只有一个get方法。

所以供应者提供了之后,就只能等着,然后消费者可以一直消费。

 

5.各种Operator

Operator有的地方翻译为操作符,有的地方翻译为操作数。

结合已有的各种xxxOperator:BinaryOperatorDoubleUnaryOperatorUnaryOperator..

翻译为"操作者",以应合提供者、消费者。

各种操作者都是有返回的。  JCP大概把难于清晰界定其作用的各种函数式接口定义为操作者。

 

6..Bi,Binary是什么鬼?

Bi

在java.util.function下带有许多Bi开头的,例如BiFunction,BiConsumer,BiPredicate,xxxBixxx。

Bi是一个前缀,源自拉丁bis,表示两个、两次、两倍,总之就是2xxx。

所以带Bi开头的都是指有2个参数,而不带Bi的对应的都是一个参数为主(目前)。看看Predicate,BiPredicate就明白了:

@FunctionalInterface
public interface BiPredicate<T, U> {
    boolean test(T t, U u);
}

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}

注:为了节省篇幅,没有复制完整的源码和注释。

 

可以看到,Predicate带一个参数,BiPredicate有2个参数。

Jcp定义了不少Bixxx,大概是因为两个参数的场景也不少,否则为什么不定义三个,四个,...n个的Predicate等接口?

为了便于交流,我有的时候称呼Bixxx之类的函数为双参函数/方法,避免有些同事不知道这是什么玩意

 

Binary

一般想到的是二进制,不过在这里Binary兼有Bi一致的含义,合起来就是2个参数,且参数类型一致(包括返回也是一样)。

拿非常典型的java.util.function.BinaryOperator<T>看下:

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

BiFunciton的定义(局部):

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

BiFunction必然是两个参数,但它不要求三个参数一致,而BinaryOperator则要求一致。

所以,这里Binary可以理解为三参一致,即两个入参和一个出参(返回值)一致,或者是两个入仓和返回值一致。

按照这个思维,其实JCP等可以考虑把这个命名为:triple

 

7.Collector(常规接口)

收集者 public interface Collector<T, A, R>

这是一个比较奇怪的接口,这是因为除了两个工具类的函数(of),其它的公共抽象方法的命名有背常规。

一般而言,方法的命名规范为:动词、动词+名词、形容词

例如 do(),doSomething(),isFinished()

但这里都是名词:supplieraccumulatorcombinerfinishercharacteristics。

分别表示提供者、累积者、合并者、完成者、特征(注意,这里是一个集合)

Collector也提供了两个公共静态函数of,用于返回一个新的Collector实现。

在of函数中,其实即使使用Collectors.CollectorImpl 来实现。

所以,如果不想开发新的Collector实现,基本上用Collectors或者Collect.of即可,但本质上都是用Collectors.CollectorImpl

这也应该是JCP所期望的吧!

 

8.Collectors(实现Collector接口为主的工具类)

这个类非常有用。

当我们使用流处理的时候,常常会有转换的需要。当转换完成后需要输出另外一个格式的时候,就需要使用到收集器。

Collectors就是一个收集器集合,包含了各种各种的收集函数,实现诸如分组、转换(目标是list,map等)等操作

收集器集合中所有的公共方法方法返回的都是收集者(Collector)类型。

当我们查看源码,可以发现一个非常重要的地方:凡是返回Collector的public static 方法,最后都会调用:

return new CollectorImpl<>(supplier, accumulator, merger, finisher, characteristics);

CollectorImpl是Collector接口的实现,属于私有静态类(由于Collectors是final导致的)

java这些写的意图应该是这样的:你们不要费心(包括继承,扩展等等),就用Collectors的具体方法即可.

以常用toList()为例:

public static <T>
    Collector<T, ?, List<T>> toList() {
        return new CollectorImpl<>(ArrayList::new, List::add,
                                   (left, right) -> { left.addAll(right); return left; },
                                   CH_ID);
    }

Collector类型的返回,可以被实现了Stream.collect利用,从而完成输出。

Stream的实现有很多,如前,最常见的是java.util.stream.ReferencePipeline<P_IN, P_OUT>

 

对于一般的工程师而言,想要Collect,可以这么做:

1.利用Collector.of构建一个,或者利用Colelctors的现货

2.把构建好的Collector当做入参,塞给Stream.collect方法.

3.完成

 

两个名词

mutable-可变的

immutable-不可变的

阅读stream代码的时候,需要格外注意哪些参数是可变的,还是不可变的。

特别注意:对于管道操作、流操作,要求部分参数不可变是一个很常见的要求。

 

1.2、java对其它接口/类的改造

主要是对集合类型的改造(从1.8开发)。

1.2.1、Colletion改造

集合类型的基类Collection新增了几个接口(J8开始):

default Stream<E> parallelStream() {
        return StreamSupport.stream(spliterator(), true);
}

@Override
default Spliterator<E> spliterator() {
        return Spliterators.spliterator(this, 0);
}

default Stream<E> stream() {
        return StreamSupport.stream(spliterator(), false);
}

上面这个Splitrator(可分割迭代器) 主要也是为了服务于并行流。如果不是采用并行流,那么也会用到,只不过不执行分隔而已(存疑)。

JCP都提供了默认实现,并为这些默认实现提供了对应的静态工具类:StreamSupport,Spliterators.

如此,所有Collection的实现类都可以使用steam,parallelStream方法。

1.2.2、Map?

Map中没有和流有关的改造,主要是对函数式编程的改造,或者具体一点就是增加了郎打的实现。

default void forEach(BiConsumer<? super K, ? super V> action) 

default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function)

default V computeIfAbsent(K key,Function<? super K, ? extends V> mappingFunction) 

default V computeIfPresent(K key,BiFunction<? super K, ? super V, ? extends V> remappingFunction)

default V compute(K key,BiFunction<? super K, ? super V, ? extends V> remappingFunction)

default V merge(K key, V value,
            BiFunction<? super V, ? super V, ? extends V> remappingFunction)

注意:这几个都是默认函数,意味着子子孙孙类都不用特别覆盖,不用特别实现,可以直接用了。

这是是顺便提一下,和流没有什么直接关系,但他们都和函数式接口相关。

 

1.3、java提供的一些新工具类

这个比较多,包括前面提到的Collectors,StreamSupport,Spliterators等,更多的可以看java.util.stream,java.util.function,java.util下有关代码。

二、简单的Collector示例

基本上,要做流处理,工程师大部分情况下都可以不要写多余代码就能实现,这是因为这些操作有共性,JAVA

提供了基本的实现,这些实现又基本覆盖了可以想象的地方。

 

如前,新增的工具类和对现有Collection的改造,使得工程师基本只要学习郎打表达式写法、工具类的使用,即可完成流操作。

在很大部分情况下,其实就是简化为对郎打语法、Collectors的学习-掌握了结果,基本在想用的时候就能用起来。

要实现Stream是如何完成Collect等,可以看Stream的标准实现类:java.util.stream.ReferencePipeline<P_IN, P_OUT>

 

示例-模仿java.util.stream.Collectors.CollectorImpl实现toList

public class ChineseArticles {
    private static List<ArticleRecord> ARTICLES;
    static {
        ARTICLES=new ArrayList<>();
        ARTICLES.add(new ArticleRecord("唐","李白","侠客行","""
                十步杀一人 千里不留行 事了拂衣去 深藏身与名
                """));        
        ARTICLES.add(new ArticleRecord("唐","李贺","马诗","大漠沙如雪"));        
    }
    @SuppressWarnings("rawtypes")
    public static ArrayList createArrayList() {
        return new ArrayList();
    }
    
    public static  void add(ArrayList<ArticleRecord> t,ArticleRecord item) {
        t.add(item);
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    public static void main(String[] args) {
        List<ArticleRecord> list = getStream().collect(Collectors.toList());
        System.out.println(list.toString());
        Collector co = new ArticleCollector(
                ChineseArticles::createArrayList,// 供应者,类型参数是 ArrayList<ArticleRecord>
                ChineseArticles::add, // 聚集着,双参数,分别是 ArrayList<ArticleRecord>,ArticleRecord
                null, // 合并者,双参数,可以不要
                null  // 完成者- 在非并行的情况下,可以不要
                );
        Stream<ArticleRecord> articleRecordStream = getStream();
        List<ArticleRecord> listUdf = (List<ArticleRecord>) articleRecordStream.collect(co);
        System.out.println(listUdf);
        
    }

    private static Stream<ArticleRecord> getStream() {
        return ARTICLES.stream().filter(article -> {
            return article.getDynasty().equals("唐") && article.getAuthor().equals("李贺");
        });
    }
}    

在这个非常简单的例子中,中间步骤是一个 filter,收尾操作是一个collect。

fitler接受Predicate的接口实现,由于Predicate是函数式接口,所以可以用郎打的方式写 :

article->{return article.getDynasty().equals("唐") && article.getAuthor().equals("李贺");}

无需单独定义一个Predicate实现类。

ArticleCollector是完全模仿CollectorImpl创建的一个Collectors.toList实现。

只不过属性characteristics=[Characteristics.IDENTITY_FINISH]

可以直接利用Collector的静态方法来返回,不必要大费周章

Collector.of(ChineseArticles::createArrayList, ChineseArticles::add, null, null,
                new Characteristics[] {Characteristics.IDENTITY_FINISH})

 

三、小结

1.java已经提供了比较完善的函数处理有关类和接口,理论上能够满足绝大部分的流处理场景

2.大部分时候,工程师只需要注意阅读Collectors,Stream,ReferencePipeline的代码,即可大体了解流是如何操作的

3.不鼓励工程师耗费大量的时间去阅读Stream的实现代码,比较复杂,对于一般的CRUD没有太大帮助。不过如果是为了提升专业技能还是有一定帮助的。

4.要熟练编写函数式代码,需要掌握函数式接口的几种实现方式  ,具体可见 JAVA基础之四-函数式接口和流的简介

或者JAVA基础之五-函数式接口的实现  (后面这个更全一些)

5.ai编程所产生的影响 -- 有好有坏

对于资深工程师而言,能够正确地在必要的环境适用流操作,他们具有辨别是非和真善美的能力。

而初学者,或者初级工程师常常几乎没有什么辨别能力。如果在工具中适用各个大厂的编程ai助手,那么在处理集合的时候,有很大

可能在绝大部分给你来一个流处理,诱惑者绝大部分的集合都一定要stream()之后再处理,这实际就是大错特错了。

所以工程师在采用任意一个技术之前必须先做几点:了解特定技术的优缺点和适用场景、了解当前业务场景需要。

现阶段的ai编程助手,的确有主意提升效率,但整体上可以看作是一个记忆词典,它们深入思考(推理)能力还是比较欠缺的。

posted @ 2024-09-15 16:26  正在战斗中  阅读(113)  评论(0编辑  收藏  举报