我在別的篇幅已經說過:流這個東西偶爾可以用用,但我個人最大的學習動力(目前)僅僅是為了有助於閱讀spring越發繁複的原始碼
本文主要介紹Stream介面(包括主要的方法)和相關的幾個主要介面:Predicate、Consumer、Supplier
還有Collector介面,Collectors工具類。
由於網上已經有太多的文章介紹,所以,本文側重點在於簡單介紹基本的概念,並不會羅列所有的介面成員的說明(這些可以看javaDoc或者
官方的javaDoc)。
雖然主要說的是流,但也順便說了一些其它東西:函式式介面之類
注:本文所相關程式碼和圖是在J17環境下獲得的。
一、重要介面、類及其定義
1.1、流直接相關介面/類
僅介紹部分:
- Stream
- Predicate
- Consumer
- Supplier
- xxxOperator
- xxxFunction
- Collector
- 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:BinaryOperator、DoubleUnaryOperator、UnaryOperator..
翻譯為"操作者",以應合提供者、消費者。
各種操作者都是有返回的。 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()
但這裡都是名詞:supplier、accumulator、combiner、finisher、characteristics。
分別表示提供者、累積者、合併者、完成者、特徵(注意,這裡是一個集合)
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基礎之五-函式式介面的實現 (後面這個更全一些)