JAVA基礎之六-Stream(流)簡介

正在战斗中發表於2024-09-15

我在別的篇幅已經說過:流這個東西偶爾可以用用,但我個人最大的學習動力(目前)僅僅是為了有助於閱讀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基礎之五-函式式介面的實現 (後面這個更全一些)

相關文章