從這一篇文章開始,我們會由淺入深,全面的學習stream API的最佳實踐(結合我的使用經驗),本想一篇寫完,但寫著寫著發現需要寫的內容太多了,所以分成一個系列慢慢來說。給大家分享我的經驗的同時,也促使我複習每一個細節,大家共同進步。
Stream是什麼Java 8新增了一個API叫做Stream ,Stream的英文可以理解為流動的液體,可能很多人一聽腦子裡的第一印象就是流式計算,不自覺地就心生畏懼,感覺非常的高深莫測。其實這就是一個輔助處理集合資料的工具類,工具的更新必然帶來的是生產力的提升,這裡的生產力代表的就是整潔優雅的程式碼,更高的靈活度,更好的效能。相信各類的技術文章(包括部落格和書籍)已經寫過無數遍了。比如下面摘錄的《Java 8實戰》關於流的描述:
流是Java API的新成員,它允許你以宣告性方式處理資料集合(透過查詢語句來表達,而不是臨時編寫一個實現)。就現在來說,你可以把它們看成遍歷資料集的高階迭代器。此外,流還可以透明地並行處理,你無需寫任何多執行緒程式碼了!
這段話的表述個人感覺類似於抓手、賦能、心智之類的PPT黑話,看著挺高階的,也能懂一些,但也不是很懂,反正如果對於不知道Stream的人,並不能建立直接的理解。
所以流到底是什麼呢?是一個介面。讓我們看看它的宣告:
public interface Stream<T> extends BaseStream<T, Stream<T>> { Stream<T> filter(Predicate<? super T> predicate); <R> Stream<R> map(Function<? super T, ? extends R> mapper); void forEach(Consumer<? super T> action); ...}
就是個介面,然後這個藉口有一些抽象方法:filter,map,forEach等等。我們可以看到有些方法返回了新的Stream,有些直接是void。這個介面用來幹什麼用呢?處理集合資料。為什麼這麼說?看下面一個Collection介面的方法:
public interface Collection<E> extends Iterable<E> { ... default Stream<E> stream() { return StreamSupport.stream(spliterator(), false); }}
那麼所有繼承了Collection的介面都可以直接建立Stream,然後再執行Stream裡面的操作。所以這麼看下來,首先得承認書中的表述是高度抽象且精煉的,這是書籍該做的事情。但從易於理解的角度,我覺得可以說是簡潔高效安全的處理集合資料的工具類。如下圖所示,Stream是一箇中間過程。
流的處理
需要注意的點首先Stream不是一個數據結構,它不儲存任何資料,它是一種資料處理工具,代表了一種能力。Stream不會對處理的資料本身做任何修改,永遠都是返回新的Stream或者最終的處理結果。Stream可以有多箇中間操作,但只能有一個終端操作,因為終端操作就求值了。一個Stream只能用一次,不能多次複用。(因為它不儲存資料,只是一個轉換能力)。能力範圍Stream隨著Java 8的釋出已經8年多了,在我有限的職業生涯裡,碰到的一些職場新人依然有些人覺得使用for或者iterator來遍歷集合更易讀易懂。但如果他真正瞭解Stream所蘊含的能力後,應該會轉變想法。下面簡單介紹一下Stream都提供了什麼樣的能力。
生成流java.util.stream.Stream#of(T... values) 。首先stream介面本身提供了一個靜態預設方法,可以直接建立,這裡的可變引數會被解析成一個數組。java.util.Collection#stream()java.util.Arrays#stream(T[] array)java.nio.file.Files#list(Path dir)java.nio.file.Files#lines(Path path)可以看到,可以操作stream的物件基本為List或者Array。
篩選和切片這可能是用得最多的功能了。對應的方法為:
filter:接受一個Predicate斷言函式,用來遍歷元素是否符合斷言條件。可以簡單地理解為一個過濾器。distinct:無引數,將所有元素去重,和資料庫的distinct關鍵詞能力一樣。limit:接受一個int型長度欄位,表示要保留多少個元素,需要注意的時候limit並不排序。skip:和limit相對應,接受一個int型長度欄位表示跳過多少個元素,也不排序。下面舉個例子:
Stream.of("d2", "a2", "b1", "a3", "c1", "a4", "a2", "a1") .filter(x -> x.startsWith("a")) .distinct() .skip(1) .limit(3) .forEach(System.out::println); }// output: a3a4a1
對映/轉換
這裡主要是map,map代表了一種對應關係,即地圖座標與實際地點的對應關係,我們有了經緯度就可以準確地找到地址,這個例子可以很形象的解釋map命名的由來和功能。
map:接受一個Function作為引數,即輸入一個值,返回另一個值,滿足轉換的語義。flatmap:同樣接受一個Function作為引數,不同的是這個Function中有一個引數是一個stream,返回的也是一個stream,意為將多個stream連成一個stream。同樣,舉個簡單的例子:
Stream.of("d2", "a2", "b1", "a3", "c1", "a4", "a2", "a1") .filter(x -> x.startsWith("a")) .map(String::toUpperCase) .forEach(System.out::println);//outputA2A3A4A2A1List<String> list = Stream.of("Hello", "world!") .map(s -> s.split("")) .flatMap(Arrays::stream) .collect(Collectors.toList());System.out.println(list);//output[H, e, l, l, o, w, o, r, l, d, !]
查詢和匹配
這裡的能力可以認為是一個加強版的contains方法,具備多種查詢匹配能力。
allMatch:返回boolean,接受一個Predicate斷言,確認全部元素均滿足這個條件則返回true,否則返回falseanyMatch:與allMatch類似,但從語義上可以區分只要任意元素滿足條件即可noneMatch:同樣,要求沒有任何元素滿足條件findFirst:返回一個Optional,裡面是滿足條件的第一個元素findAny:返回Optional,裡面是滿足條件的任一元素這裡需要解惑的是findAny與findFirst的區別,因為這兩個都是找到滿足條件的元素就返回,但findFirst會在限制並行流的計算,會嚴格按照集合中元素的順序來依次查詢。findAny就不會有這個限制。如果非平行計算的場景,這二者並無區別。
boolean b1 = Stream.of("d2", "a2", "b1", "a3", "c1", "a4", "a2", "a1") .anyMatch(x -> x.startsWith("a")); System.out.println(b1);//output: trueString s2 = Stream.of("d2", "a2", "b1", "a3", "c1", "a4", "a2", "a1") .filter(x -> x.startsWith("a")) .findFirst() .get(); System.out.println(s2);//output: a2String s3 = Stream.of("d2", "a2", "b1", "a3", "c1", "a4", "a2", "a1") .filter(x -> x.startsWith("a")) .findAny() .get(); System.out.println(s3);//output: a2//換成並行流String s4 = Stream.of("d2", "a2", "b1", "a3", "c1", "a4", "a2", "a1") .parallel() .filter(x -> x.startsWith("a")) .findFirst() .get(); System.out.println(s4);//output: a2String s5 = Stream.of("d2", "a2", "b1", "a3", "c1", "a4", "a2", "a1") .parallel() .filter(x -> x.startsWith("a")) .findAny() .get(); System.out.println(s5);//output: a4
歸約歸約是一個比較複雜的數學理論,通常是用於將一個未知的問題轉換成另一些已知問題,同時這些已知的問題和未知的問題存在某種關聯。這裡不做詳細探討。在Stream API有一些方法就是用的類似的歸約的思想,將大的集合計算分解成小的函式計算並最終合成結果。
reducecollect這兩個方法都很重要,且都是終端操作,執行完即返回流的計算結果。我們逐個來說,先看reduce。reduce的英文含義為減少、歸納,在stream介面中的定義如下:
T reduce(T identity, BinaryOperator<T> accumulator);Optional<T> reduce(BinaryOperator<T> accumulator);<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);
這樣的方法簽名如同天書,先看一個簡單的例子:
Integer i = Stream.of(1, 4, 6, 7, 9).reduce(1, (sum, i) -> sum + i);System.out.println(i);
其中reduce我傳了2個引數:
1表示初始值,可以不給,不給的話預設從流的第一個元素開始計算,但返回是就是Optional(sum, i) -> sum + i表示計算函式,每次計算的結果都會暫存在sum中,i則是下一個元素所以總的來說,這是一個迭代歸納的過程,將多個元素的流按照自己制定的計算規則變成一個元素。不僅僅可以做述職運算,也可以實現複雜物件的轉換,先看例子(此例來源於與廖雪峰老師的網站並稍做修改,具體連結:https://www.liaoxuefeng.com/wiki/1252599548343744/1322402971648033):
List<String> props = Lists.newArrayList("profile=native", "debug=true", "logging=warn", "interval=500"); Map<String, String> map = props.stream() .map(kv -> { String[] ss = kv.split("=", 2); Map<String, String> m = Maps.newHashMap(); m.put(ss[0], ss[1]); return m; }) .reduce(new HashMap<>(), (m, kv) -> { m.putAll(kv); return m; }); map.forEach((k, v) -> System.out.println(k + " = " + v));//output:logging = warninterval = 500debug = trueprofile = native
第一個map執行完之後返回了多個小map,這裡使用reduce進行一個map的累加:
new HashMap<>()是初始值,一個空map(m, kv) ->中,m是暫存累加結果,kv表示下一個元素map以上看來,reduce的使用場景應該會很廣泛,尤其是多個集合合成一個大集合的場景。
對於多執行緒的場景,reduce也是支援的,這裡先引用一段設計者的話來輔助說明:
One of the design principles of the Streams API is that the API shouldn't differ between sequential and parallel streams, or put another way, a particular API shouldn't prevent a stream from running correctly either sequentially or in parallel.
他的意思是說,stream API的設計原則就是讓這些方法在順序執行和並行執行的場景下使用體驗一致。其實要讓這些方法高度封裝,他們在底層實現平行計算,在外面用起來感覺跟單執行緒一樣。reduce就實現了這一點,用到是第三個方法簽名:
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);
這裡相對之前的簽名多了一個combiner,可以直接從字面上理解就是組合多個執行緒的結果,但為什麼是個BinaryOperator呢?先看一個簡化版的實現:
U reduce(I, (U, T) -> U, (U, U) -> U)
I依然是初始值(U, T) -> U表示歸納的計算方法,值得注意的是這裡可以允許傳一個比的物件進來,但最終是返回一個U(U, U) -> U這個很關鍵,這個算式告訴多個執行緒怎麼組合各自的計算結果,所以應該和上面的計算方法保持一致,返回的值也保持一致
結合一個具體的例子看看:
List<User> users = Arrays.asList(new User("John", 30), new User("Julie", 35));int computedAges = users.stream().reduce(0, (partialAgeResult, user) -> partialAgeResult + user.getAge(), Integer::sum);
(partialAgeResult, user) -> partialAgeResult + user.getAge()表示計算所有人年齡的總和Integer::sum則告訴多個執行緒,把各個執行緒的計算結果相加,因為這裡是在計算加和。如果計算乘積,這裡就應該是(a,b)->a*b機智的你一定發現了,這裡沒有並行啊,只是單執行緒順序執行。沒錯,這就是設計理念的體現,單執行緒多執行緒體驗一致。這裡變成平行計算只需要這樣:List<User> users = Arrays.asList(new User("John", 30), new User("Julie", 35));int computedAges = users.stream().parallel().reduce(0, (partialAgeResult, user) -> partialAgeResult + user.getAge(), Integer::sum);
執行結果不變。如果你不嫌麻煩,可以只用這一個reduce方法,但可能會帶來一些可讀性的障礙。
因為collect要說的東西也非常多,受限於篇幅影響,我放在下一篇。
小結本文介紹了stream是什麼、建立stream的方法、stream的一些基本API的能力和reduce方法的使用。作為stream最佳實踐的開篇,先從stream的基礎開始寫,後續會逐步深入並總結我個人使用下來的最佳實踐,希望大家持續關注,共同學習。