由淺入深體驗(yàn) Stream 流
由淺入深體驗(yàn) Stream 流
Stream 流的分類(lèi)、接口、相關(guān) API 操作以及并行流的使用
????Stream 流是 Java 8 新提供給開(kāi)發(fā)者的一組操作集合的 API,將要處理的元素集合看作一種流,流在管道中傳輸,并且可以在管道的節(jié)點(diǎn)上進(jìn)行處理,比如篩選、排序、聚合等。元素流在管道中經(jīng)過(guò)中間操作(intermediate operation)的處理,最后由終端操作 (terminal operation) 得到前面處理的結(jié)果。Stream 流可以極大的提高開(kāi)發(fā)效率,也可以使用它寫(xiě)出更加簡(jiǎn)潔明了的代碼。我自從接觸過(guò) Stream 流之后,可以說(shuō)對(duì)它愛(ài)不釋手。本文將由淺及深帶您體驗(yàn) Stream 流的使用。那么就讓我們從流的簡(jiǎn)單使用來(lái)開(kāi)啟體驗(yàn)之旅。
流的簡(jiǎn)單使用
????本節(jié)將通過(guò)實(shí)際的例子帶您一起了解 Stream 流:創(chuàng)建流以及簡(jiǎn)單的使用,并且將其與 Java 8 之前的實(shí)現(xiàn)方式做一下對(duì)比。
????我們將創(chuàng)建一個(gè)學(xué)生類(lèi)?student,它包含姓名?name?和分?jǐn)?shù)?score?兩個(gè)屬性。并且初始化一個(gè)學(xué)生的集合,然后分別通過(guò) Stream 流和 Java 7 的集合操作實(shí)現(xiàn)篩選未及格(分?jǐn)?shù)<60 分)的學(xué)生名單。
創(chuàng)建流
????有以下兩種創(chuàng)建流的方式,第一種方式我們使用的會(huì)相對(duì)較多。
調(diào)用集合的? stream()?方法或者?parallelStream()?方法創(chuàng)建流。Stream 類(lèi)的靜態(tài)? of()?方法創(chuàng)建流。
清單 1. 創(chuàng)建 Stream 流
List?createStream?=?new?ArrayList();
//?順序流
Stream?stream?=?createStream.stream();
//?并行流
Stream?parallelStream?=?createStream.parallelStream();
//?of()方法創(chuàng)建
Stream?stringStream?=?Stream.of(
???????createStream.toArray(new?String[createStream.size()]));
使用流
????清單2展示了如何使用 Stream 流篩選未及格學(xué)生名單:
清單 2. 使用 Stream 流篩選未及格學(xué)生名單
public?static?void?streamImpl(List?students)?{
??List?filterStudent?=?students.stream()
???????.filter(one?->?one.getScore()??60).collect(Collectors.toList());
??System.out.println(filterStudent);
}
????而使用 Java 7 實(shí)現(xiàn)篩選未及格學(xué)生名單所需代碼相對(duì)冗長(zhǎng),如清單3所示:
清單 3. Java 7 實(shí)現(xiàn)篩選未及格學(xué)生名單
public?static?void?java7Impl(List?students)?{
??List?filterStudent?=?new?ArrayList<>();
??for?(Student?student?:?students)?{
?????if?(student.getScore()?60)?{
????????filterStudent.add(student);
?????}
??}
??System.out.println(filterStudent);
}
????對(duì)比兩段代碼,我們很容易看出來(lái) Stream 流可以讓我操作集合的代碼更加簡(jiǎn)潔,而且可以很清晰地體現(xiàn)出來(lái)我們是在做一個(gè)篩選的動(dòng)作,在某些情況下可以讓我們的代碼更加易讀。
流的基礎(chǔ)知識(shí)
????接下來(lái)您將了解 Stream 流的基礎(chǔ)知識(shí),這部分的內(nèi)容將有助于您理解流的相關(guān)操作。
流的分類(lèi)
????Stream?流分為順序流和并行流,所謂順序流就是按照順序?qū)现械脑剡M(jìn)行處理,而并行流則是使用多線(xiàn)程同時(shí)對(duì)集合中多個(gè)元素進(jìn)行處理,所以在使用并行流的時(shí)候就要注意線(xiàn)程安全的問(wèn)題了。
終端操作和中間操作
????終端操作會(huì)消費(fèi) Stream 流,并且會(huì)產(chǎn)生一個(gè)結(jié)果,比如?iterator()?和?spliterator()。如果一個(gè) Stream 流被消費(fèi)過(guò)了,那它就不能被重用的。
????中間操作會(huì)產(chǎn)生另一個(gè)流。需要注意的是中間操作不是立即發(fā)生的。而是當(dāng)在中間操作創(chuàng)建的新流上執(zhí)行完終端操作后,中間操作指定的操作才會(huì)發(fā)生。流的中間操作還分無(wú)狀態(tài)操作和有狀態(tài)操作兩種。
在無(wú)狀態(tài)操作中,在處理流中的元素時(shí),會(huì)對(duì)當(dāng)前的元素進(jìn)行單獨(dú)處理。比如,過(guò)濾操作,因?yàn)槊總€(gè)元素都是被單獨(dú)進(jìn)行處理的,所有它和流中的其它元素?zé)o關(guān)。 在有狀態(tài)操作中,某個(gè)元素的處理可能依賴(lài)于其他元素。比如查找最小值,最大值,和排序,因?yàn)樗麄兌家蕾?lài)于其他的元素。
流接口
????下面是一張 Stream 的 UML (統(tǒng)一建模語(yǔ)言) 類(lèi)圖,后文會(huì)講解其中的一些關(guān)鍵方法。
圖 1. Stream UML 類(lèi)圖

BaseStream 接口
????從上面的 UML 圖可以看出來(lái)?BaseStream?接口是 Stream 流最基礎(chǔ)的接口,它提供了所有流都可以使用的基本功能。BaseStream?是一個(gè)泛型接口,它有兩個(gè)類(lèi)型參數(shù)?T?和?S?, 其中?T?指定了流中的元素的類(lèi)型,?S?指定了具體流的類(lèi)型,由??可以知道?>S?必須為?BaseStream?或?BaseStream?子類(lèi),換句話(huà)說(shuō),就是?S?必須是擴(kuò)展自?BaseStream?的。BaseStream?繼承了?AutoCloseable?接口,簡(jiǎn)化了關(guān)閉資源的操作,但是像平時(shí)我們操作的集合或數(shù)組,基本上都不會(huì)出現(xiàn)關(guān)閉流的情況。下面是?BaseStream?接口下定義的方法的相關(guān)解釋:
Iterator?:獲取流的迭代器。iterator() Spliterator spliterator()?:獲取流的?spliterator?。boolean isParallel()?:判斷一個(gè)流是否是并行流,如果是則返回?true?,否則返回?false?。S sequential()?:基于調(diào)用流返回一個(gè)順序流,如果調(diào)用流本身就是順序流,則返回其本身。S parallel()?:基于調(diào)用流,返回一個(gè)并行流,如果調(diào)用流本身就是并行流,則返回其本身。S unordered()?:基于調(diào)用流,返回一個(gè)無(wú)序流。S onClose(Runnable closeHandler)?:返回一個(gè)新流,?closeHandler?指定了該流的關(guān)閉處理程序,當(dāng)關(guān)閉該流時(shí),將調(diào)用這個(gè)處理程序。void close()?:從?AutoCloseable?繼承來(lái)的,調(diào)用注冊(cè)關(guān)閉處理程序,關(guān)閉調(diào)用流(很少會(huì)被使用到)。
????清單 4 列舉了由 BaseStream 接口派生出來(lái)的流接口,包括了?IntStream,LongStream,?Stream?以及?DoubleStream。其中 Stream 接口最為通用,本文的主要講解對(duì)象也是它。
清單 4. 由?BaseStream?接口派生出的流接口
public?interface?IntStream?extends?BaseStream<、Intege、r,?IntStream>?{}
public?interface?LongStream?extends?BaseStream?{}
public?interface?DoubleStream?extends?BaseStream?{}
public?interface?Stream?extends?BaseStream>?{}
Stream 接口
Stream filter(Predicate predicate)?:產(chǎn)生一個(gè)新流,其中包含調(diào)用流中滿(mǎn)足?predicate?指定的謂詞元素,即篩選符合條件的元素后重新生成一個(gè)新的流。(中間操作)Stream map(Function mapper)?,產(chǎn)生一個(gè)新流,對(duì)調(diào)用流中的元素應(yīng)用?mapper?,新?Stream?流中包含這些元素。(中間操作)IntStream mapToInt(ToIntFunction mapper)?:對(duì)調(diào)用流中元素應(yīng)用?mapper?,產(chǎn)生包含這些元素的一個(gè)新?IntStream?流。(中間操作)Stream sorted(Comparator comparator)?:產(chǎn)生一個(gè)自然順序排序或者指定排序條件的新流。(中間操作)void forEach(Consumer action)?:遍歷了流中的元素。(終端操作)Optional min(Comparator comparator)?和?Optional max(Comparator comparator)?:獲得流中最大最小值,比較器可以由自己定義。(終端操作)boolean anyMatch(Predicate super T> predicate)?:判斷?Stream?流中是否有任何符合要求的元素,如果有則返回?ture,沒(méi)有返回?false?。(終端操作)Stream?,去重操作,將?distinct() Stream?流中的元素去重后,返回一個(gè)新的流。(中間操作)
流的 API 操作
縮減操作
????什么是縮減操作呢?最終將流縮減為一個(gè)值的終端操作,我們稱(chēng)之為縮減操作。在上一節(jié)中提到的?min(),max()?方法返回的是流中的最小或者最大值,這兩個(gè)方法屬于特例縮減操作。而通用的縮減操作就是指的我們的?reduce()?方法了,在 Stream 類(lèi)中?reduce?方法有三種簽名方法,如下所示:
清單 5.?reduce()?方法的三種實(shí)現(xiàn)
public?interface?Stream?extends?BaseStream>?{
?...
Optional?reduce(BinaryOperator?accumulator);
T?reduce(T?identity,?BinaryOperator?accumulator);
?U?reduce(U?identity,
?????????????????BiFunction?accumulator,
?????????????????BinaryOperator?combiner);
?...
}
????由上面的代碼可以看出,在 Stream API 中?reduce()?方法一共存在著三種簽名,而這三種簽名則分別會(huì)適用在不同的場(chǎng)景,我們下面就一起來(lái)看一下如何使用。
第一種簽名
在下面的代碼中我們將對(duì)一個(gè) Integer 類(lèi)型的集合做求和操作。
清單 6. 第一種簽名的?reduce()?的使用
public?static?void?reduceFirstSign()?{
??List?list?=?Arrays.asList(1,2,3,4,5,6);
??ptional?count?=?list.stream().reduce((a,?b)?->?(a?+?b));
??System.out.println(count.get());?//?21
}
第二種簽名
????與第一種簽名不同的是多接收了一個(gè)參數(shù)?identity?,在首次執(zhí)行?accumulator?表達(dá)式的時(shí)候它的第一個(gè)參數(shù)并不是 Stream 流的第一個(gè)元素,而是?identity?。比如下面的例子最終輸出的結(jié)果是 Stream 流中所有元素乘積的 2 倍。
清單 7. 第二種簽名的?reduce()?的使用
public?static?void?reduceSecondSign()?{
??List?list?=?Arrays.asList(1,2,3,4,5,6);
??Integer?count?=?list.stream().reduce(2,?(a,?b)?->?(a?*?b));
??System.out.println(count);??//?1440
}
第三種簽名
????前面兩種前面的一個(gè)缺點(diǎn)在于返回的數(shù)據(jù)都只能和 Stream 流中元素類(lèi)型一致,但這在某些情況下是無(wú)法滿(mǎn)足我們的需求的,比如 Stream 流中元素都是?Integer?類(lèi)型,但是求和之后數(shù)值超過(guò)了?Integer?能夠表示的范圍,需要使用?Long?類(lèi)型接受,這就用到了我們第三種簽名的?reduce()?方法。
清單 8. 第三種簽名的?reduce()?的使用
public?static?void?reduceThirdSign()?{
??List?list?=?Arrays.asList(Integer.MAX_VALUE,?Integer.MAX_VALUE);
??long?count?=?list.stream().reduce(0L,?(a,?b)?->?(a?+?b),?(a,b)?->?0L);
??System.out.println(count);
}
????總的來(lái)說(shuō)縮減操作有兩個(gè)特點(diǎn),一是他只返回一個(gè)值,二是它是一個(gè)終端操作。在這里順便給大家留一個(gè)縮減操作的題目,統(tǒng)計(jì)一個(gè)班上所有及格同學(xué)的分?jǐn)?shù)總。
映射
????可能在我們的日常開(kāi)發(fā)過(guò)程中經(jīng)常會(huì)遇到將一個(gè)集合轉(zhuǎn)換成另外一個(gè)對(duì)象的集合,那么這種操作放到 Stream 流中就是映射操作。映射操作主要就是將一個(gè) Stream 流轉(zhuǎn)換成另外一個(gè)對(duì)象的 Stream 流或者將一個(gè) Stream 流中符合條件的元素放到一個(gè)新的 Stream 流里面。
????在 Stream API 庫(kù)中也提供了豐富的 API 來(lái)支持我們的映射操作,清單 9 中的方法都是我們所講的映射操作。
清單 9. 映射操作相關(guān)方法定義
public?interface?Stream?extends?BaseStream>?{
???...
???Stream?map(Function?super?T,???extends?R>?mapper);
??IntStream?mapToInt(ToIntFunction?super?T>?mapper);
??LongStream?mapToLong(ToLongFunction?super?T>?mapper);
??DoubleStream?mapToDouble(ToDoubleFunction?super?T>?mapper);
???Stream?flatMap(Function?super?T,???extends?Stream?extends?R>>>?mapper);
??IntStream?flatMapToInt(Function?super?T,???extends?IntStream>?mapper);
??LongStream?flatMapToLong(Function?super?T,???extends?LongStream>?mapper);
??DoubleStream?flatMapToDouble(Function?super?T,???extends?DoubleStream>?mapper);
???...
}
????其中最通用的應(yīng)該就屬?mapv?和?flatMap?兩個(gè)方法了,下面將以不同的例子分別來(lái)講解著兩個(gè)方法。
map()
????map()?方法可以將一個(gè)流轉(zhuǎn)換成另外一種對(duì)象的流,其中的?T?是原始流中元素的類(lèi)型,而?R?則是轉(zhuǎn)換之后的流中元素的類(lèi)型。通過(guò)下面的代碼我們將一個(gè)學(xué)生對(duì)象的 Stream 流轉(zhuǎn)換成一個(gè)?Double?類(lèi)型(學(xué)生的分?jǐn)?shù))的 Stream 流并求和后輸出。
清單 10.?map()?方法的使用示例
public?static?void?useMap()?{
??List?students?=?initData();
??double?scoreCount?=?students.stream()
????????????.map(Student::getScore)
????????????.reduce(0.0,?(a,b)?->?(a?+?b));
??System.out.println(scoreCount);
}
????當(dāng)然上面這種情況用?mapToDouble()?會(huì)更加方便,使用?map()?是為了展示一下?map?的使用方式,那么使用?mapToDouble()?方法的代碼如下:
清單 11.?mapToDouble()?方法的使用示例
double?scoreCount?=?students.stream()
????????????????.mapToDouble(Student::getScore)
????????????????.sum();
flatMap()
????flatMap()?操作能把原始流中的元素進(jìn)行一對(duì)多的轉(zhuǎn)換,并且將新生成的元素全都合并到它返回的流里面。假如現(xiàn)每個(gè)班的學(xué)生都學(xué)了不同的課程,現(xiàn)在需要統(tǒng)計(jì)班里所有學(xué)生所學(xué)的課程列表,該如何實(shí)現(xiàn)呢?
清單 12.?flatMap ()方法的使用示例
public?static?void?useFlatMap()?{
??List?students?=?initData();
??List?course?=?students.stream().flatMap(one?->?one.getCourse().stream()).distinct()
????????????????.collect(Collectors.toList());
??System.out.println(course);
}
????如上代碼中?flatMap()?中返回的是一個(gè)一個(gè)的?String?類(lèi)型的 Stream 流,它們會(huì)被合并到最終返回的 Stream 流(String 類(lèi)型)中。而后面的?distinct()?則是一個(gè)去重的操作,?collect()?是收集操作。
收集操作
????很多時(shí)候我們需要從流中收集起一些元素,并以集合的方式返回,我們把這種反向操作稱(chēng)為收集操作。對(duì)于收集操作,Stream API 也提供了相應(yīng)的方法。
清單 13. 收集操作相關(guān) API
public?interface?Stream?extends?BaseStream>?{
?...
?R?collect(Collector?super?T,?A,?R>?collector);
?...
}
????其中?R?指定結(jié)果的類(lèi)型,?T?指定了調(diào)用流的元素類(lèi)型。內(nèi)部積累的類(lèi)型由?A?指定。collector?是一個(gè)收集器,指定收集過(guò)程如何執(zhí)行,?collect()?方法是一個(gè)終端方法。一般情況我們只需要借助?Collectors?中的方法就可以完成收集操作。
????Collectors?類(lèi)是一個(gè)最終類(lèi),里面提供了大量的靜態(tài)的收集器方法,借助他,我們基本可以實(shí)現(xiàn)各種復(fù)雜的功能了。
清單 14. Collectors
public?final?class?Collectors?{
...
public?static??Collector>?toList()?{
...
}
public?static??Collector>?toMap(
Function?super?T,???extends?K>?keyMapper,
Function?super?T,???extends?U>?valueMapper)?{
??...
}
...
}
????Collectors?給我們提供了非常豐富的收集器,這里只列出來(lái)了?toList?和?toMap?兩種,其他的可以參考?Collectors?類(lèi)的源碼。toList()?相信您在清單 14 中已經(jīng)見(jiàn)到了,那么下面將展示如何將一個(gè)使用收集操作將一個(gè)?List?集合轉(zhuǎn)為?Map?。
清單 15. 使收集操作將 List 轉(zhuǎn) Map
public?static?void?list2Map()?{
??List?students?=?initData();
??Map?collect?=?students.stream()
?????????.collect(Collectors.toMap(one?->?one.getName(),
one?->?one.getScore()));
??System.out.println(collect);
}
????可以看到通過(guò) Stream API 可以很方便地將一個(gè)?List?轉(zhuǎn)成了?Map?,但是這里有一個(gè)地方需要注意。那就是在通過(guò) Stream API 將?List?轉(zhuǎn)成?Map?的時(shí)候我們需要確保?key?不會(huì)重復(fù),否則轉(zhuǎn)換的過(guò)程將會(huì)直接拋出異常。
并行流的使用
????我們處于一個(gè)多核處理器的時(shí)代,在日常的開(kāi)發(fā)過(guò)程中也經(jīng)常會(huì)接觸到多線(xiàn)程。Stream API 也提供了相應(yīng)的并行流來(lái)支持我們并行地操作數(shù)組和集合框架,從而高速地執(zhí)行我們對(duì)數(shù)組或者集合的一些操作。
????其實(shí)創(chuàng)建一個(gè)并行流非常簡(jiǎn)單,在創(chuàng)建流部分已經(jīng)提到過(guò)如何創(chuàng)建一個(gè)并行流,我們只需要調(diào)用集合的?parallelStream()?方法就可以輕松的得到一個(gè)并行流。相信大家也知道多線(xiàn)程編程非常容易出錯(cuò),所以使用并行流也有一些限制,一般來(lái)說(shuō),應(yīng)用到并行流的任何操作都必須符合三個(gè)約束條件:無(wú)狀態(tài)、不干預(yù)、關(guān)聯(lián)性。因?yàn)檫@三大約束確保在并行流上執(zhí)行操作的結(jié)果和在順序流上執(zhí)行的結(jié)果是相同的。
????在縮減操作部分我們一共提到了三種簽名的?reduce()?方法,其中第三種簽名的?reduce()?方法最適合與并行流結(jié)合使用。
清單 16. 第三種簽名方式的?reduce()?方法與并行流結(jié)合使用
public?interface?Stream?extends?BaseStream>?{
?...
?U?reduce(U?identity,
?????????????????BiFunction?accumulator,
?????????????????BinaryOperator?combiner);
?...
}
????其中?accumulator?被為累加器,?combiner?為合成器。combiner?定義的函數(shù)將?accumulator?提到的兩個(gè)值合并起來(lái),在之前的例子中我們沒(méi)有為合并器設(shè)置具體的表達(dá)式,因?yàn)樵谀莻€(gè)場(chǎng)景下我們不會(huì)使用到合并器。下面我們來(lái)看一個(gè)例子,并且分析其執(zhí)行的步驟:
清單 17. 并行流使用場(chǎng)景
public?static?void?main(String[]?args)?{
??List?list?=?Arrays.asList(2,2);
??Integer?result?=?list.stream().parallel().reduce(2,?(a,?b)?->?(a?+?b),?(a,?b)?->?(a?+?b));
??System.out.println(result);
}
????上面的代碼實(shí)際上是先使用累加器把 Stream 流中的兩個(gè)元素都加?2?后,然后再使用合并器將兩部分的結(jié)果相加。最終得到的結(jié)果也就是?8?。并行流的使用場(chǎng)景也不光是在這中縮減操作上,比如我會(huì)經(jīng)常使用并行流處理一些復(fù)雜的對(duì)象集合轉(zhuǎn)換,或者是一些必須循環(huán)調(diào)用的網(wǎng)絡(luò)請(qǐng)求等等,當(dāng)然在使用的過(guò)程中最需要注意的還是線(xiàn)程安全問(wèn)題。
參考答案
????在流的 API 操作 章節(jié)給大家留了一個(gè)統(tǒng)計(jì)一個(gè)班上所有及格同學(xué)的分?jǐn)?shù)總和的題目,此處給出我的實(shí)現(xiàn)方式,第一種方式是使用?reduce()?方法實(shí)現(xiàn),也就是我們留題目的地方所講解的 API 方法:
清單 18. 第一種實(shí)現(xiàn)方式
public?static?void?answer()?{
????List?students?=?initData();
????Double?result?=?students.stream()
????????????.filter(one?->?one.getScore()?>=?60).map(o?->?o.getScore())
????????????.reduce(0d,?(a,b)?->?(a?+?b));
????System.out.println(result);
}
????第二種實(shí)現(xiàn)方法是通過(guò)?sum()?方法實(shí)現(xiàn),?sum()?也是一個(gè)終端操作,它可以對(duì)一個(gè)數(shù)字類(lèi)型的流進(jìn)行求和操作并返回結(jié)果:
清單 19. 第二種實(shí)現(xiàn)方式
public?static?void?answerSecondImpl()?{
????????List?students?=?initData();
????????Double?result?=?students.stream()
????????????????.filter(one?->?one.getScore()?>=?60).mapToDouble(o?->?o.getScore()).sum();
????????System.out.println(result);
}
????以上是我提供的兩種解題方式,如果您有更好的解決方法歡迎以評(píng)論的方式共享給大家。
結(jié)束語(yǔ)
????在本教程中,我們主要了解了 Java 8 Stream 流的基礎(chǔ)知識(shí)及使用,涵蓋 Stream 流的分類(lèi)、接口、相關(guān) API 操作以及并行流的使用。
