1兜挨、什么是Stream
Java 8之前的集合類(lèi)庫(kù)主要依賴(lài)于 外部迭代(external iteration)座享。 Collection
實(shí)現(xiàn) Iterable
接口,從而使得用戶(hù)可以依次遍歷集合的元素堰乔。比如我們需要把一個(gè)集合中的形狀都設(shè)置成紅色虱肄,那么可以這么寫(xiě):
for (Shape shape : shapes) {
shape.setColor(RED);
}
這個(gè)例子演示了外部迭代:for-each循環(huán)調(diào)用 shapes
的 iterator()
方法進(jìn)行依次遍歷等曼。外部循環(huán)的代碼非常直接片排,但它有如下問(wèn)題:
- Java的for循環(huán)是串行的加袋,而且必須按照集合中元素的順序進(jìn)行依次處理盯蝴;
- 集合框架無(wú)法對(duì)控制流進(jìn)行優(yōu)化毅哗,例如通過(guò)排序、并行捧挺、短路(short-circuiting)求值以及惰性求值改善性能虑绵。
盡管有時(shí)for-each循環(huán)的這些特性(串行,依次)是我們所期待的闽烙,但它對(duì)改善性能造成了阻礙翅睛。
我們可以使用內(nèi)部迭代(internal iteration)替代外部迭代,用戶(hù)把對(duì)迭代的控制權(quán)交給類(lèi)庫(kù)黑竞,并向類(lèi)庫(kù)傳遞迭代時(shí)所需執(zhí)行的代碼捕发,java 8中的內(nèi)部迭代通過(guò)訪問(wèn)者模式(Visitor)實(shí)現(xiàn)。
下面是前例的內(nèi)部迭代代碼:
shapes.forEach(s -> s.setColor(RED));
盡管看起來(lái)只是一個(gè)小小的語(yǔ)法改動(dòng)很魂,但是它們的實(shí)際差別非常巨大扎酷。用戶(hù)把對(duì)操作的控制權(quán)交還給類(lèi)庫(kù),從而允許類(lèi)庫(kù)進(jìn)行各種各樣的優(yōu)化(例如亂序執(zhí)行遏匆、惰性求值和并行等等)法挨∷瘢總的來(lái)說(shuō),內(nèi)部迭代使得外部迭代中不可能實(shí)現(xiàn)的優(yōu)化成為可能凡纳。
外部迭代同時(shí)承擔(dān)了 做什么(把形狀設(shè)為紅色)和 怎么做(得到 Iterator實(shí)例然后依次遍歷)兩項(xiàng)職責(zé)窃植,而內(nèi)部迭代只負(fù)責(zé) 做什么,而把 怎么做 留給類(lèi)庫(kù)荐糜。通過(guò)這樣的職責(zé)轉(zhuǎn)變:用戶(hù)的代碼會(huì)變得更加清晰巷怜,而類(lèi)庫(kù)則可以進(jìn)行各種優(yōu)化,從而使所有用戶(hù)都從中受益暴氏。
Java 8 API添加了一個(gè)新的抽象稱(chēng)為Stream延塑,Stream 使用一種類(lèi)似用 SQL 語(yǔ)句從數(shù)據(jù)庫(kù)查詢(xún)數(shù)據(jù)的直觀方式來(lái)提供一種對(duì) Java 集合運(yùn)算和表達(dá)的高階抽象。
這種風(fēng)格將要處理的元素集合看作一種流偏序, 流在管道中傳輸页畦, 并且可以在管道的節(jié)點(diǎn)上進(jìn)行處理, 比如篩選研儒, 排序豫缨,聚合等。
Stream 不是集合元素端朵,它不是數(shù)據(jù)結(jié)構(gòu)并不保存數(shù)據(jù)好芭,它是有關(guān)算法和計(jì)算的,它更像一個(gè)高級(jí)版本的 Iterator冲呢。原始版本的 Iterator舍败,用戶(hù)只能顯式地一個(gè)一個(gè)遍歷元素并對(duì)其執(zhí)行某些操作;高級(jí)版本的 Stream敬拓,用戶(hù)只要給出需要對(duì)其包含的元素執(zhí)行什么操作邻薯,比如 “過(guò)濾掉長(zhǎng)度大于 10 的字符串”、“獲取每個(gè)字符串的首字母”等乘凸,Stream 會(huì)隱式地在內(nèi)部進(jìn)行遍歷厕诡,做出相應(yīng)的數(shù)據(jù)轉(zhuǎn)換。
Stream 就如同一個(gè)迭代器(Iterator)营勤,單向灵嫌,不可往復(fù),數(shù)據(jù)只能遍歷一次葛作,遍歷過(guò)一次后即用盡了寿羞,就好比流水從面前流過(guò),一去不復(fù)返赂蠢。
而和迭代器又不同的是绪穆,Stream 可以并行化操作,迭代器只能命令式地、串行化操作霞幅。顧名思義漠吻,當(dāng)使用串行方式去遍歷時(shí)量瓜,每個(gè) item 讀完后再讀下一個(gè) item司恳。而使用并行去遍歷時(shí),數(shù)據(jù)會(huì)被分成多個(gè)段绍傲,其中每一個(gè)都在不同的線程中處理扔傅,然后將結(jié)果一起輸出。Stream 的并行操作依賴(lài)于 Java7 中引入的 Fork/Join
框架來(lái)拆分任務(wù)和加速處理過(guò)程烫饼。
2猎塞、流的構(gòu)成與轉(zhuǎn)換
當(dāng)我們使用一個(gè)流的時(shí)候,通常包括三個(gè)基本步驟:
- 獲取一個(gè)數(shù)據(jù)源(source)
- 數(shù)據(jù)轉(zhuǎn)換
- 執(zhí)行操作獲取想要的結(jié)果杠纵,
每次轉(zhuǎn)換原有 Stream 對(duì)象不改變荠耽,返回一個(gè)新的 Stream 對(duì)象(可以有多次轉(zhuǎn)換),這就允許對(duì)其操作可以像鏈條一樣排列比藻,變成一個(gè)管道铝量,如下圖所示:
流的操作類(lèi)型分為兩種:
- Intermediate:一個(gè)流可以后面跟隨零個(gè)或多個(gè) intermediate 操作。其目的主要是打開(kāi)流银亲,做出某種程度的數(shù)據(jù)映射/過(guò)濾慢叨,然后返回一個(gè)新的流,交給下一個(gè)操作使用务蝠。這類(lèi)操作都是惰性化的(lazy)拍谐,就是說(shuō),僅僅調(diào)用到這類(lèi)方法馏段,并沒(méi)有真正開(kāi)始流的遍歷轩拨。
-
Terminal:一個(gè)流只能有一個(gè) terminal 操作,當(dāng)這個(gè)操作執(zhí)行后院喜,流就被使用“光”了亡蓉,無(wú)法再被操作。所以這必定是流的最后一個(gè)操作够坐。Terminal 操作的執(zhí)行寸宵,才會(huì)真正開(kāi)始流的遍歷,并且會(huì)生成一個(gè)結(jié)果元咙,或者一個(gè) side effect梯影。
在對(duì)于一個(gè) Stream 進(jìn)行多次轉(zhuǎn)換操作 (Intermediate 操作),每次都對(duì) Stream 的每個(gè)元素進(jìn)行轉(zhuǎn)換庶香,而且是執(zhí)行多次甲棍,這樣時(shí)間復(fù)雜度就是 N(轉(zhuǎn)換次數(shù))個(gè) for 循環(huán)里把所有操作都做掉的總和嗎?其實(shí)不是這樣的赶掖,轉(zhuǎn)換操作都是 lazy 的感猛,多個(gè)轉(zhuǎn)換操作只會(huì)在 Terminal 操作的時(shí)候融合起來(lái)七扰,一次循環(huán)完成。我們可以這樣簡(jiǎn)單的理解陪白,Stream 里有個(gè)操作函數(shù)的集合颈走,每次轉(zhuǎn)換操作就是把轉(zhuǎn)換函數(shù)放入這個(gè)集合中,在 Terminal 操作的時(shí)候循環(huán) Stream 對(duì)應(yīng)的集合咱士,然后對(duì)每個(gè)元素執(zhí)行所有的函數(shù)立由。
還有一種操作被稱(chēng)為 short-circuiting
(短路)。用以指:
- 對(duì)于一個(gè) intermediate 操作序厉,如果它接受的是一個(gè)無(wú)限大(infinite/unbounded)的 Stream锐膜,但返回一個(gè)有限的新 Stream。
- 對(duì)于一個(gè) terminal 操作弛房,如果它接受的是一個(gè)無(wú)限大的 Stream道盏,但能在有限的時(shí)間計(jì)算出結(jié)果。
當(dāng)操作一個(gè)無(wú)限大的 Stream文捶,而又希望在有限時(shí)間內(nèi)完成操作荷逞,則在管道內(nèi)擁有一個(gè) short-circuiting
操作是必要非充分條件。
int sum = widgets.stream().filter(w -> w.getColor() == RED).mapToInt(w -> w.getWeight()).sum();
stream()
獲取source拄轻,filter
和 mapToInt
為 intermediate 操作颅围,進(jìn)行數(shù)據(jù)篩選和轉(zhuǎn)換,最后一個(gè) sum()
為 terminal 操作恨搓,對(duì)符合條件的作重量求和院促。
下面提供最常見(jiàn)的幾種構(gòu)造 Stream 的樣例。
// 1. Individual values
Stream stream = Stream.of("a", "b", "c");
// 2. Arrays
String[] strArray = new String[] { "a", "b", "c" };
stream = Stream.of(strArray);
stream = Arrays.stream(strArray);
// 3. Collections
List<String> list = Arrays.asList(strArray);
stream = list.stream();
需要注意的是斧抱,對(duì)于基本數(shù)值型常拓,目前有三種對(duì)應(yīng)的包裝類(lèi)型 Stream:
IntStream
、LongStream
辉浦、DoubleStream
弄抬。當(dāng)然我們也可以用 Stream<Integer>
、Stream<Long>
宪郊、
Stream<Double>
掂恕,但是 boxing 和 unboxing 會(huì)很耗時(shí),所以特別為這三種基本數(shù)值型提供了對(duì)應(yīng)的 Stream弛槐。
Java 8 中還沒(méi)有提供其它數(shù)值型 Stream懊亡,因?yàn)檫@將導(dǎo)致擴(kuò)增的內(nèi)容較多。而常規(guī)的數(shù)值型聚合運(yùn)算可以通過(guò)上面三種 Stream 進(jìn)行乎串。
數(shù)值流的構(gòu)造:
IntStream.of(new int[] { 1, 2, 3 }).forEach(System.out::println);
IntStream.range(1, 3).forEach(System.out::println);
IntStream.rangeClosed(1, 3).forEach(System.out::println);
流轉(zhuǎn)換為其它數(shù)據(jù)結(jié)構(gòu):
// 1. Array
String[] strArray1 = stream.toArray(String[]::new);
// 2. Collection
List<String> list1 = stream.collect(Collectors.toList());
List<String> list2 = stream.collect(Collectors.toCollection(ArrayList::new));
Set set1 = stream.collect(Collectors.toSet());
Stack stack1 = stream.collect(Collectors.toCollection(Stack::new));
// 3. String
String str = stream.collect(Collectors.joining()).toString();
一個(gè) Stream 只可以使用一次店枣,上面的代碼為了簡(jiǎn)潔而重復(fù)使用了數(shù)次。
3、流的操作
接下來(lái)鸯两,當(dāng)把一個(gè)數(shù)據(jù)結(jié)構(gòu)包裝成 Stream 后闷旧,就要開(kāi)始對(duì)里面的元素進(jìn)行各類(lèi)操作了。常見(jiàn)的操作可以歸類(lèi)如下钧唐。
- Intermediate: map (mapToInt, flatMap 等)忙灼、 filter、 distinct逾柿、 sorted缀棍、 peek宅此、 limit机错、 skip、 parallel父腕、 sequential弱匪、 unordered
- Terminal:forEach、 forEachOrdered璧亮、 toArray萧诫、 reduce、 collect枝嘶、 min帘饶、 max、 count群扶、 anyMatch及刻、 allMatch、 noneMatch竞阐、 findFirst缴饭、 findAny、 iterator
- Short-circuiting:anyMatch骆莹、 allMatch颗搂、 noneMatch、 findFirst幕垦、 findAny丢氢、 limit
我們下面看一下 Stream 的比較典型用法。
3.1 map/flatMap
我們先來(lái)看 map
先改。如果你熟悉 scala 這類(lèi)函數(shù)式語(yǔ)言疚察,對(duì)這個(gè)方法應(yīng)該很了解,它的作用就是把 input Stream 的每一個(gè)元素盏道,映射成 output Stream 的另外一個(gè)元素稍浆。
List<String> output = wordList.stream().map(String::toUpperCase).collect(Collectors.toList());
// 這段代碼把所有的單詞轉(zhuǎn)換為大寫(xiě)
List<Integer> nums = Arrays.asList(1, 2, 3, 4);
List<Integer> squareNums = nums.stream().map(n -> n * n).collect(Collectors.toList());
// 這段代碼生成一個(gè)整數(shù) list 的平方數(shù) {1, 4, 9, 16}
從上面例子可以看出,map
生成的是個(gè) 1:1 映射,每個(gè)輸入元素衅枫,都按照規(guī)則轉(zhuǎn)換成為另外一個(gè)元素嫁艇。還有一些場(chǎng)景,是一對(duì)多映射關(guān)系的弦撩,這時(shí)需要 flatMap
步咪。
Stream<List<Integer>> inputStream = Stream.of(Arrays.asList(1), Arrays.asList(2, 3), Arrays.asList(4, 5, 6));
Stream<Integer> outputStream = inputStream.flatMap((childList) -> childList.stream());
flatMap
把 input Stream 中的層級(jí)結(jié)構(gòu)扁平化,就是將最底層元素抽出來(lái)放到一起益楼,最終 output 的新 Stream 里面已經(jīng)沒(méi)有 List 了猾漫,都是直接的數(shù)字。
3.2 filter
filter
對(duì)原始 Stream 進(jìn)行某項(xiàng)測(cè)試感凤,通過(guò)測(cè)試的元素被留下來(lái)生成一個(gè)新 Stream悯周。
Integer[] sixNums = { 1, 2, 3, 4, 5, 6 };
Integer[] evens = Stream.of(sixNums).filter(n -> n % 2 == 0).toArray(Integer[]::new);
// 經(jīng)過(guò)條件“被 2 整除”的 filter,剩下的數(shù)字為 {2, 4, 6}
List<String> output = reader.lines().flatMap(line -> Stream.of(line.split(REGEXP)))
.filter(word -> word.length() > 0).collect(Collectors.toList());
這段代碼首先把每行的單詞用 flatMap
整理到新的 Stream陪竿,然后保留長(zhǎng)度不為 0 的禽翼,就是整篇文章中的全部單詞了。
3.3 forEach
forEach
方法接收一個(gè) Lambda 表達(dá)式族跛,然后在 Stream 的每一個(gè)元素上執(zhí)行該表達(dá)式闰挡。
// Java 8
roster.stream().filter(p -> p.getGender() == Person.Sex.MALE).forEach(p -> System.out.println(p.getName()));
// Pre-Java 8
for (Person p : roster) {
if (p.getGender() == Person.Sex.MALE) {
System.out.println(p.getName());
}
}
對(duì)一個(gè)人員集合遍歷,找出男性并打印姓名礁哄〕ば铮可以看出來(lái),forEach 是為 Lambda 而設(shè)計(jì)的桐绒,保持了最緊湊的風(fēng)格夺脾。而且 Lambda 表達(dá)式本身是可以重用的,非常方便掏膏。當(dāng)需要為多核系統(tǒng)優(yōu)化時(shí)劳翰,可以 parallelStream().forEach()
,只是此時(shí)原有元素的次序沒(méi)法保證馒疹,并行的情況下將改變串行時(shí)操作的行為佳簸,此時(shí) forEach 本身的實(shí)現(xiàn)不需要調(diào)整,而 Java8 以前的 for 循環(huán) code 可能需要加入額外的多線程邏輯颖变。
但一般認(rèn)為生均,forEach
和常規(guī) for 循環(huán)的差異不涉及到性能,它們僅僅是函數(shù)式風(fēng)格與傳統(tǒng) Java 風(fēng)格的差別腥刹。
另外一點(diǎn)需要注意马胧,forEach 是 terminal 操作,因此它執(zhí)行后衔峰,Stream 的元素就被“消費(fèi)”掉了佩脊,你無(wú)法對(duì)一個(gè) Stream 進(jìn)行兩次 terminal 運(yùn)算蛙粘。下面的代碼是錯(cuò)誤的:
stream.forEach(element -> doOneThing(element));
stream.forEach(element -> doAnotherThing(element));
相反,具有相似功能的 intermediate 操作 peek
可以達(dá)到上述目的威彰。如下是出現(xiàn)在該 api javadoc 上的一個(gè)示例出牧。
peek 對(duì)每個(gè)元素執(zhí)行操作并返回一個(gè)新的 Stream:
Stream.of("one", "two", "three", "four").filter(e -> e.length() > 3)
.peek(e -> System.out.println("Filtered value: " + e)).map(String::toUpperCase)
.peek(e -> System.out.println("Mapped value: " + e)).collect(Collectors.toList());
forEach
不能修改自己包含的本地變量值,也不能用 break/return 之類(lèi)的關(guān)鍵字提前結(jié)束循環(huán)歇盼。
3.4 findFirst
這是一個(gè) termimal 兼 short-circuiting 操作舔痕,它總是返回 Stream 的第一個(gè)元素,或者空豹缀。
這里比較重點(diǎn)的是它的返回值類(lèi)型:Optional
伯复。這也是一個(gè)模仿 Scala 語(yǔ)言中的概念,作為一個(gè)容器邢笙,它可能含有某值啸如,或者不包含。使用它的目的是盡可能避免 NullPointerException鸣剪。
Optional
的兩個(gè)用例:
public static final void main(String[] args) {
String strA = " abcd ", strB = null;
print(strA);
print("");
print(strB);
getLength(strA);
getLength("");
getLength(strB);
}
public static void print(String text) {
// Java 8
Optional.ofNullable(text).ifPresent(System.out::println);
// Pre-Java 8
if (text != null) {
System.out.println(text);
}
}
public static int getLength(String text) {
// Java 8
return Optional.ofNullable(text).map(String::length).orElse(-1);
// Pre-Java 8
// return if (text != null) ? text.length() : -1;
};
在更復(fù)雜的 if (xx != null)
的情況中组底,使用 Optional
代碼的可讀性更好,而且它提供的是編譯時(shí)檢查筐骇,能極大的降低 NPE 這種 Runtime Exception 對(duì)程序的影響,或者迫使程序員更早的在編碼階段處理空值問(wèn)題江滨,而不是留到運(yùn)行時(shí)再發(fā)現(xiàn)和調(diào)試铛纬。
Stream 中的 findAny、max/min唬滑、reduce 等方法等返回 Optional
值告唆。還有例如 IntStream.average()
返回 OptionalDouble
等等。
3.5 reduce
這個(gè)方法的主要作用是把 Stream 元素組合起來(lái)晶密。它提供一個(gè)起始值(種子)擒悬,然后依照運(yùn)算規(guī)則(BinaryOperator),和前面 Stream 的第一個(gè)稻艰、第二個(gè)懂牧、第 n 個(gè)元素組合。從這個(gè)意義上說(shuō)尊勿,字符串拼接僧凤、數(shù)值的 sum、min元扔、max躯保、average 都是特殊的 reduce
。例如 Stream 的 sum 就相當(dāng)于
Integer sum = integers.reduce(0, (a, b) -> a + b);
// or
Integer sum = integers.reduce(0, Integer::sum);
也有沒(méi)有起始值的情況澎语,這時(shí)會(huì)把 Stream 的前面兩個(gè)元素組合起來(lái)途事,返回的是 Optional
验懊。
reduce
的用例:
// 字符串連接,concat = "ABCD"
String concat = Stream.of("A", "B", "C", "D").reduce("", String::concat);
// 求最小值尸变,minValue = -3.0
double minValue = Stream.of(-1.5, 1.0, -3.0, -2.0).reduce(Double.MAX_VALUE, Double::min);
// 求和鲁森,sumValue = 10, 有起始值
int sumValue = Stream.of(1, 2, 3, 4).reduce(0, Integer::sum);
// 求和,sumValue = 10, 無(wú)起始值
sumValue = Stream.of(1, 2, 3, 4).reduce(Integer::sum).get();
// 過(guò)濾振惰,字符串連接歌溉,concat = "ace"
concat = Stream.of("a", "B", "c", "D", "e", "F").filter(x -> x.compareTo("Z") > 0).reduce("", String::concat);
上面代碼例如第一個(gè)示例的 reduce()
,第一個(gè)參數(shù)(空白字符)即為起始值骑晶,第二個(gè)參數(shù)(String::concat
)為 BinaryOperator
痛垛。這類(lèi)有起始值的 reduce()
都返回具體的對(duì)象。而對(duì)于第四個(gè)示例沒(méi)有起始值的 reduce()
桶蛔,由于可能沒(méi)有足夠的元素匙头,返回的是 Optional
,請(qǐng)留意這個(gè)區(qū)別仔雷。
3.6 limit/skip
limit
返回 Stream 的前面 n 個(gè)元素蹂析;skip
則是扔掉前 n 個(gè)元素(它是由一個(gè)叫 subStream 的方法改名而來(lái))。
limit
和 skip
對(duì)運(yùn)行次數(shù)的影響
public void testLimitAndSkip() {
List<Person> persons = new ArrayList();
for (int i = 1; i <= 10000; i++) {
Person person = new Person(i, "name" + i);
persons.add(person);
}
List<String> personList2 = persons.stream().map(Person::getName).limit(10).skip(3).collect(Collectors.toList());
System.out.println(personList2);
}
private class Person {
public int no;
private String name;
public Person(int no, String name) {
this.no = no;
this.name = name;
}
public String getName() {
System.out.println(name);
return name;
}
}
輸出結(jié)果為:
name1
name2
name3
name4
name5
name6
name7
name8
name9
name10
[name4, name5, name6, name7, name8, name9, name10]
這是一個(gè)有 10碟婆,000 個(gè)元素的 Stream电抚,但在 short-circuiting 操作 limit
和 skip
的作用下,管道中 map
操作指定的 getName()
方法的執(zhí)行次數(shù)為 limit
所限定的 10 次竖共,而最終返回結(jié)果在跳過(guò)前 3 個(gè)元素后只有后面 7 個(gè)返回蝙叛。
有一種情況是 limit/skip 無(wú)法達(dá)到 short-circuiting 目的的,就是把它們放在 Stream 的排序操作后公给,原因跟 sorted
這個(gè) intermediate 操作有關(guān):此時(shí)系統(tǒng)并不知道 Stream 排序后的次序如何借帘,所以 sorted
中的操作看上去就像完全沒(méi)有被 limit
或者 skip
一樣。
limit 和 skip 對(duì) sorted 后的運(yùn)行次數(shù)無(wú)影響淌铐。
List<Person> persons = new ArrayList();for(
int i = 1;i<=5;i++)
{
Person person = new Person(i, "name" + i);
persons.add(person);
}
List<Person> personList2 = persons.stream().sorted((p1, p2) -> p1.getName().compareTo(p2.getName())).limit(2)
.collect(Collectors.toList());System.out.println(personList2);
上面的代碼首先對(duì) 5 個(gè)元素的 Stream 排序肺然,然后進(jìn)行 limit
操作。輸出結(jié)果為:
name2
name1
name3
name2
name4
name3
name5
name4
[stream.StreamDWPerson@87aac27]
即雖然最后的返回元素?cái)?shù)量是 2腿准,但整個(gè)管道中的 sorted
表達(dá)式執(zhí)行次數(shù)沒(méi)有像前面例子相應(yīng)減少际起。
最后有一點(diǎn)需要注意的是,對(duì)一個(gè) parallel 的 Steam 管道來(lái)說(shuō)释涛,如果其元素是有序的加叁,那么 limit
操作的成本會(huì)比較大,因?yàn)樗姆祷貙?duì)象必須是前 n 個(gè)也有一樣次序的元素唇撬。取而代之的策略是取消元素間的次序它匕,或者不要用 parallel Stream。
3.7 sorted
對(duì) Stream 的排序通過(guò) sorted
進(jìn)行窖认,它比數(shù)組的排序更強(qiáng)之處在于可以首先對(duì) Stream 進(jìn)行各類(lèi) map
豫柬、filter
告希、limit
、skip
甚至 distinct 來(lái)減少元素?cái)?shù)量后烧给,再排序燕偶,這能幫助程序明顯縮短執(zhí)行時(shí)間。
優(yōu)化:排序前進(jìn)行 limit
和 skip
List<Person> persons = new ArrayList();
for (int i = 1; i <= 5; i++) {
Person person = new Person(i, "name" + i);
persons.add(person);
}
List<Person> personList2 = persons.stream().limit(2).sorted((p1, p2) -> p1.getName().compareTo(p2.getName()))
.collect(Collectors.toList());
System.out.println(personList2);
結(jié)果會(huì)簡(jiǎn)單很多:
name2
name1
[stream.StreamDWPerson@53d8d10a]
當(dāng)然础嫡,這種優(yōu)化是有 business logic 上的局限性的:即不要求排序后再取值指么。
3.8 min/max/distinct
min
和 max
的功能也可以通過(guò)對(duì) Stream 元素先排序,再 findFirst
來(lái)實(shí)現(xiàn)榴鼎,但前者的性能會(huì)更好伯诬,為 O(n)
,而 sorted
的成本是 O(n log n)
巫财。同時(shí)它們作為特殊的 reduce
方法被獨(dú)立出來(lái)也是因?yàn)榍笞畲笞钚≈凳呛艹R?jiàn)的操作盗似。
找出最長(zhǎng)一行的長(zhǎng)度:
BufferedReader br = new BufferedReader(new FileReader("c:\\SUService.log"));
int longest = br.lines().mapToInt(String::length).max().getAsInt();
br.close();
System.out.println(longest);
下面的例子則使用 distinct 來(lái)找出不重復(fù)的單詞。
找出全文的單詞平项,轉(zhuǎn)小寫(xiě)赫舒,并排序:
List<String> words = br.lines().flatMap(line -> Stream.of(line.split(" "))).filter(word -> word.length() > 0)
.map(String::toLowerCase).distinct().sorted().collect(Collectors.toList());
br.close();
System.out.println(words);
3.9 match
Stream 有三個(gè) match 方法,從語(yǔ)義上說(shuō):
- allMatch:Stream 中全部元素符合傳入的 predicate闽瓢,返回 true
- anyMatch:Stream 中只要有一個(gè)元素符合傳入的 predicate接癌,返回 true
- noneMatch:Stream 中沒(méi)有一個(gè)元素符合傳入的 predicate,返回 true
它們都不是要遍歷全部元素才能返回結(jié)果鸳粉。例如 allMatch
只要一個(gè)元素不滿(mǎn)足條件扔涧,就 skip
剩下的所有元素,返回 false弯汰。
List<Person> persons = new ArrayList();
persons.add(new Person(1, "name" + 1, 10));
persons.add(new Person(2, "name" + 2, 21));
persons.add(new Person(3, "name" + 3, 34));
persons.add(new Person(4, "name" + 4, 6));
persons.add(new Person(5, "name" + 5, 55));
boolean isAllAdult = persons.stream().allMatch(p -> p.getAge() > 18);
System.out.println("All are adult? " + isAllAdult);
boolean isThereAnyChild = persons.stream().anyMatch(p -> p.getAge() < 12);
System.out.println("Any child? " + isThereAnyChild);
輸出結(jié)果:
All are adult? false
Any child? true
3.10 Stream.generate
通過(guò)實(shí)現(xiàn) Supplier
接口,可以自己來(lái)控制流的生成咏闪。這種情形通常用于隨機(jī)數(shù)鸽嫂、常量的 Stream纵装,或者需要前后元素間維持著某種狀態(tài)信息的 Stream据某。把 Supplier
實(shí)例傳遞給 Stream.generate()
生成的 Stream癣籽,默認(rèn)是串行(相對(duì) parallel 而言)但無(wú)序的(相對(duì) ordered
而言)。由于它是無(wú)限的瓶籽,在管道中塑顺,必須利用 limit
之類(lèi)的操作限制 Stream 大小严拒。
生成 10 個(gè)隨機(jī)整數(shù):
Random seed = new Random();
Supplier<Integer> random = seed::nextInt;
Stream.generate(random).limit(10).forEach(System.out::println);
// Another way
IntStream.generate(() -> (int) (System.nanoTime() % 100)).limit(10).forEach(System.out::println);
Stream.generate()
還接受自己實(shí)現(xiàn)的 Supplier
糙俗。例如在構(gòu)造海量測(cè)試數(shù)據(jù)的時(shí)候赊颠,用某種自動(dòng)的規(guī)則給每一個(gè)變量賦值劈彪;或者依據(jù)公式計(jì)算 Stream 的每個(gè)元素值痘括。這些都是維持狀態(tài)信息的情形滔吠。
自實(shí)現(xiàn) Supplier
:
Stream.generate(new PersonSupplier()).limit(10)
.forEach(p -> System.out.println(p.getName() + ", " + p.getAge()));
private class PersonSupplier implements Supplier<Person> {
private int index = 0;
private Random random = new Random();
@Override
public Person get() {
return new Person(index++, "StormTestUser" + index, random.nextInt(100));
}
}
輸出結(jié)果:
StormTestUser1, 9
StormTestUser2, 12
StormTestUser3, 88
StormTestUser4, 51
StormTestUser5, 22
StormTestUser6, 28
StormTestUser7, 81
StormTestUser8, 51
StormTestUser9, 4
StormTestUser10, 76
3.11 Stream.iterate
iterate
跟 reduce
操作很像翰舌,接受一個(gè)種子值椅贱,和一個(gè) UnaryOperator
(例如 f)只冻。然后種子值成為 Stream 的第一個(gè)元素山橄,f(seed) 為第二個(gè)驾胆,f(f(seed)) 第三個(gè)丧诺,以此類(lèi)推驳阎。
生成一個(gè)等差數(shù)列:
Stream.iterate(0, n -> n + 3).limit(10). forEach(x -> System.out.print(x + " "));
輸出結(jié)果:
0 3 6 9 12 15 18 21 24 27
與 Stream.generate
相仿呵晚,在 iterate
時(shí)候管道必須有 limit
這樣的操作來(lái)限制 Stream 大小饵隙。
4金矛、并行處理
4.1 并行流
Stream接口可以通過(guò)收集源調(diào)用parallelStream
方法來(lái)把集合轉(zhuǎn)換為并行流驶俊。并行流就是把一個(gè)內(nèi)容分成多個(gè)數(shù)據(jù)塊榕酒,并用不同的線程分別處理每個(gè)數(shù)據(jù)塊的流想鹰。
并行流內(nèi)部使用了默認(rèn)的ForkJoinPool
杖挣,它默認(rèn)的線程數(shù)量就是處理器數(shù)量,這個(gè)值是由
Runtime.getRunTime().availableProcessors()
得到的筐乳。但是可以通過(guò)系統(tǒng)屬性 java.util.concurrent.ForkJoinPool.common.parallelism
來(lái)改變線程池大小蝙云。如下:
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism","12");
使用正確的數(shù)據(jù)結(jié)構(gòu)然后使其并行化工作能保證最佳的性能波材。特別注意原始類(lèi)型的裝箱和拆箱操作廷区。
一些幫你決定某個(gè)特定情況下是否有必要使用并行流的建議:
- 有疑問(wèn)隙轻,則親自測(cè)試驗(yàn)證效果玖绿。把順序流轉(zhuǎn)化成并行流輕而易舉斑匪,但卻不一定是好事蚀瘸。并行流并不總是比順序流快。
- 留意裝箱衙猪。自動(dòng)裝箱和拆箱操作會(huì)大大降低性能垫释。Java8中有原始類(lèi)型流(
IntStream
,LongStream
,DoubleStream
)來(lái)避免這種操作棵譬,但凡有可能應(yīng)該使用這些流订咸。 - 有些操作本身在并行流上的性能就比順序流差。特別是
limit
和findFirst
等依賴(lài)于元素順序的操作父叙。 - 還要考慮流的操作流水線的總計(jì)算成本趾唱。
- 對(duì)于較小的數(shù)據(jù)量甜癞,選擇并行幾乎從來(lái)都不是一個(gè)好的決定带欢。并行處理少數(shù)幾個(gè)元素的好處還抵不上并行化造成的額外開(kāi)銷(xiāo)吁朦。
- 要考慮流背后的數(shù)據(jù)結(jié)構(gòu)是否易于分解逗宜。如
ArrayList
的拆分效率比LinkList
高得多纺讲,因?yàn)榍罢哂貌恢闅v就可以平均拆分熬甚,而后者則必須遍歷蚀浆。另外,用range
工廠方法創(chuàng)建的原始類(lèi)型流也可以快速分解敷扫。
4.2 分支/合并框架
分支合并框架的目的是以遞歸方式將可以并行的任務(wù)拆分成更小的任務(wù)葵第,然后將每個(gè)子任務(wù)的結(jié)果合并起來(lái)生成整體結(jié)果。這是ExecutorService
接口的一個(gè)實(shí)現(xiàn),它把子任務(wù)分配給線程池(稱(chēng)為ForkJoinPool
)中的工作線程恭朗。
要把任務(wù)提交到這個(gè)池痰腮,必須創(chuàng)建RecursiveTask<R>
的一個(gè)子類(lèi)棍丐,其中R
是并行化任務(wù)(以及所有子任務(wù))產(chǎn)生的結(jié)果類(lèi)型歌逢,或者如果任務(wù)不返回結(jié)果秘案,則是RecursiveAction
類(lèi)型(當(dāng)然它可能會(huì)更新其他非局部機(jī)構(gòu))。要定義RecursiveTask
赤惊,只需要實(shí)現(xiàn)它唯一的抽象方法:
protected abstract R compute();
這個(gè)方法同時(shí)定義了將任務(wù)拆分成子任務(wù)的邏輯未舟,以及無(wú)法再拆分或不方便再拆分時(shí)处面,生成單個(gè)子任務(wù)結(jié)果的邏輯。偽代碼:
if(任務(wù)足夠小或不可分){
順序計(jì)算該任務(wù)
}else{
將任務(wù)分成兩個(gè)子任務(wù)
遞歸調(diào)用本方法野揪,拆分每個(gè)子任務(wù)斯稳,等待所有子任務(wù)完成
合并每個(gè)子任務(wù)的結(jié)果
}
選個(gè)例子為基礎(chǔ),讓我們?cè)囍眠@個(gè)框架為一個(gè)數(shù)字范圍(這里用一個(gè)long[]數(shù)組表示)求和憎茂。需要先為RecursiveTask
類(lèi)做一個(gè)實(shí)現(xiàn):
public class ForkJoinSumCalculator extends java.util.concurrent.RecursiveTask<Long> {
private final long[] numbers;
private final int start;
private final int end;
public static final long THRESHOLD = 10_000;
public ForkJoinSumCalculator(long[] numbers) {
this(numbers, 0, numbers.length);
}
private ForkJoinSumCalculator(long[] numbers, int start, int end) {
this.numbers = numbers;
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
int length = end - start;
if (length <= THRESHOLD) {
return computeSequentially();
}
ForkJoinSumCalculator leftTask = new ForkJoinSumCalculator(numbers, start, start + length / 2);
leftTask.fork();
ForkJoinSumCalculator rightTask = new ForkJoinSumCalculator(numbers, start + length / 2, end);
Long rightResult = rightTask.compute();
Long leftResult = leftTask.join();
return leftResult + rightResult;
}
private long computeSequentially() {
long sum = 0;
for (int i = start; i < end; i++) {
{
sum += numbers[i];
}
return sum;
}
}
}
測(cè)試方法:
public static long forkJoinSum(long n) {
long[] numbers = LongStream.rangeClosed(1, n).toArray();
ForkJoinTask<Long> task = new ForkJoinSumCalculator(numbers);
return new ForkJoinPool().invoke(task);
}
運(yùn)行ForkJoinSumCalculator
當(dāng)把ForkJoinSumCalculator
任務(wù)傳給ForkJoinPool
時(shí),這個(gè)任務(wù)就由池中的一個(gè)線程執(zhí)行募逞,這個(gè)線程會(huì)調(diào)用任務(wù)的compute
方法放接。該方法會(huì)檢查任務(wù)是否小到足以順序執(zhí)行,如果不夠小則會(huì)把要求和的數(shù)組分成兩半乳乌,分給兩個(gè)新的ForkJoinSumCalculator
汉操,而它們也由ForkJoinPool
安排執(zhí)行磷瘤。因此,這一過(guò)程可以遞歸重復(fù)扳抽,把原任務(wù)分為更小的任務(wù),直到滿(mǎn)足不方便或不可能再進(jìn)一步拆分的條件楞陷。這時(shí)會(huì)順序計(jì)算每個(gè)任務(wù)的結(jié)果固蛾,然后由分支過(guò)程創(chuàng)建的(隱含的)任務(wù)二叉樹(shù)遍歷回到它的根昌犹。接下來(lái)會(huì)合并每個(gè)子任務(wù)的部分結(jié)果,從而得到總?cè)蝿?wù)的結(jié)果沧竟。
分支/合并框架工程使用了一種稱(chēng)為工作竊取的技術(shù)。在實(shí)際應(yīng)用中糕非,這意味著這些任務(wù)差不多被平均分配到ForkJoinPool
中的所有線程上。每個(gè)線程都為分配給它的任務(wù)保存一個(gè)雙向鏈?zhǔn)疥?duì)列衡招,每完成一個(gè)任務(wù)始腾,就會(huì)從隊(duì)列頭上取出下一個(gè)任務(wù)開(kāi)始執(zhí)行∧唐埽基于一些原因驼抹,某個(gè)線程可能早早完成了分配給它的任務(wù),也就是它的隊(duì)列已經(jīng)空了明也,而其它的線程還很忙温数。這時(shí)鹉胖,這個(gè)線程并沒(méi)有閑下來(lái)甫菠,而是隨機(jī)選了一個(gè)別的線程從隊(duì)列的尾巴上‘偷走’一個(gè)任務(wù)。這個(gè)過(guò)程一直繼續(xù)下去痰洒,直到所有的任務(wù)都執(zhí)行完畢,所有的隊(duì)列都清空仓犬。這就是為什么要?jiǎng)澇稍S多小任務(wù)而不是少數(shù)幾個(gè)大任務(wù),這有助于更好地工作線程之間平衡負(fù)載叽躯。
4.3 Spliterator
Spliterator
是Java 8中加入的另一個(gè)新接口点骑;這個(gè)名字代表“可分迭代器”(splitable iterator)。和Iterator
一樣袁辈,Spliterator
也用于遍歷數(shù)據(jù)源中的元素尾膊,但它是為了并行執(zhí)行而設(shè)計(jì)的。雖然在實(shí)踐中可能用不著自己開(kāi)發(fā)Spliterator
抓谴,但了解一下它的實(shí)現(xiàn)方式會(huì)讓你對(duì)并行流的工作原理有更深入的了解。Java8已經(jīng)為集合框架中包含的所有數(shù)據(jù)結(jié)構(gòu)提供了一個(gè)默認(rèn)的Spliterator
實(shí)現(xiàn)。集合實(shí)現(xiàn)了Spliterator
接口慎宾,接口提供了一個(gè)spliterator
方法。這個(gè)接口定義了若干方法汹碱,如下面的代碼清單所示。
public interface Spliterator<T> {
boolean tryAdvance(Consumer<? super T> action);
Spliterator<T> trySplit();
long estimateSize();
int characteristics();
}
與往常一樣,T是Spliterator
遍歷的元素的類(lèi)型冲茸。tryAdvance
方法的行為類(lèi)似于普通的因?yàn)樗鼤?huì)按順序一個(gè)一個(gè)使用Spliterator
中的元素,并且如果還有其他元素要遍歷就返回true逗栽。但trySplit
是專(zhuān)為Spliterator
接口設(shè)計(jì)的苍凛,因?yàn)樗梢园岩恍┰貏澇鋈シ纸o第二個(gè)Spliterator
(由該方法返回),讓它們兩個(gè)并行處理悠栓。Spliterator
還可通過(guò)estimateSize
方法估計(jì)還剩下多少元素要遍歷惭适,因?yàn)榧词共荒敲创_切,能快速算出來(lái)是一個(gè)值也有助于讓拆分均勻一點(diǎn)。
5戒突、Collector
前面的代碼中可以發(fā)現(xiàn),stream里有一個(gè)collect(Collector c)
方法隔崎,接收一個(gè)Collector
實(shí)例窑多, 其功能是把stream歸約成一個(gè)value的操作仍稀,這里的value可以是一個(gè)Collection、Map等對(duì)象埂息。
歸約技潘,就是對(duì)中間操作(過(guò)濾,轉(zhuǎn)換等)的結(jié)果進(jìn)行收集歸一化的步驟享幽,當(dāng)然也可以對(duì)歸約結(jié)果進(jìn)行再歸約,這就是歸約的嵌套了拾弃。中間操作不消耗流值桩,歸約會(huì)消耗流,而且只能消費(fèi)一次豪椿,就像把流都吃掉了奔坟。
5.1 轉(zhuǎn)換成其他集合
toList
示例:
List<Integer> collectList = Stream.of(1, 2, 3, 4).collect(Collectors.toList());
System.out.println("collectList: " + collectList);
// 打印結(jié)果
// collectList: [1, 2, 3, 4]
toSet
示例:
Set<Integer> collectSet = Stream.of(1, 2, 3, 4).collect(Collectors.toSet());
System.out.println("collectSet: " + collectSet);
// 打印結(jié)果
// collectSet: [1, 2, 3, 4]
通常情況下携栋,創(chuàng)建集合時(shí)需要調(diào)用適當(dāng)?shù)臉?gòu)造函數(shù)指明集合的具體類(lèi)型:
List<Artist> artists = new ArrayList<>();
但是調(diào)用toList
或者toSet
方法時(shí),不需要指定具體的類(lèi)型咳秉,Stream類(lèi)庫(kù)會(huì)自動(dòng)推斷并生成合適的類(lèi)型婉支。當(dāng)然,有時(shí)候我們對(duì)轉(zhuǎn)換生成的集合有特定要求澜建,比如向挖,希望生成一個(gè)TreeSet,而不是由Stream類(lèi)庫(kù)自動(dòng)指定的一種類(lèi)型炕舵。此時(shí)使用toCollection
何之,它接受一個(gè)函數(shù)作為參數(shù), 來(lái)創(chuàng)建集合咽筋。
值得我們注意的是溶推,Collectors的源碼,因?yàn)槠浣邮艿暮瘮?shù)參數(shù)必須繼承于Collection奸攻,也就是意味著Collection并不能轉(zhuǎn)換所有的繼承類(lèi)悼潭,最明顯的就是不能通過(guò)toCollection
轉(zhuǎn)換成Map。
如果生成一個(gè)Map舞箍,我們需要調(diào)用toMap
方法。由于Map中有Key和Value這兩個(gè)值皆疹,故該方法與toSet
疏橄、toList
等的處理方式是不一樣的。toMap
最少應(yīng)接受兩個(gè)參數(shù)略就,一個(gè)用來(lái)生成key捎迫,另外一個(gè)用來(lái)生成value。
public Map<Long, Account> getIdAccountMap(List<Account> accounts) {
return accounts.stream().collect(Collectors.toMap(Account::getId, account -> account));
}
account -> account
是一個(gè)返回本身的Lambda表達(dá)式表牢,其實(shí)還可以使用Function接口中的一個(gè)默認(rèn)方法代替窄绒,使整個(gè)方法更簡(jiǎn)潔優(yōu)雅:
public Map<Long, Account> getIdAccountMap(List<Account> accounts) {
return accounts.stream().collect(Collectors.toMap(Account::getId, Function.identity()));
}
重復(fù)key的情況:
public Map<String, Account> getNameAccountMap(List<Account> accounts) {
return accounts.stream().collect(Collectors.toMap(Account::getUsername, Function.identity()));
}
這個(gè)方法可能報(bào)錯(cuò)(java.lang.IllegalStateException: Duplicate key
),因?yàn)閚ame是有可能重復(fù)的崔兴。toMap
有個(gè)重載方法彰导,可以傳入一個(gè)合并的函數(shù)來(lái)解決key沖突問(wèn)題:
public Map<String, Account> getNameAccountMap(List<Account> accounts) {
return accounts.stream()
.collect(Collectors.toMap(Account::getUsername, Function.identity(), (key1, key2) -> key2));
}
這里只是簡(jiǎn)單的使用后者覆蓋前者來(lái)解決key重復(fù)問(wèn)題。
toMap
還有另一個(gè)重載方法敲茄,可以指定一個(gè)Map的具體實(shí)現(xiàn)位谋,來(lái)收集數(shù)據(jù):
public Map<String, Account> getNameAccountMap(List<Account> accounts) {
return accounts.stream().collect(
Collectors.toMap(Account::getUsername, Function.identity(), (key1, key2) -> key2, LinkedHashMap::new));
}
5.2 轉(zhuǎn)成值
使用collect
可以將Stream轉(zhuǎn)換成值。例如堰燎,maxBy
和minBy
允許用戶(hù)按照某個(gè)特定的順序生成一個(gè)值掏父。
Optional<Integer> collectMaxBy = Stream.of(1, 2, 3, 4)
.collect(Collectors.maxBy(Comparator.comparingInt(o -> o)));
System.out.println("collectMaxBy:" + collectMaxBy.get());
5.3 數(shù)據(jù)分組
collect
的一個(gè)常用操作將Stream分解成兩個(gè)集合。假如一個(gè)數(shù)字的Stream秆剪,我們可能希望將其分割成兩個(gè)集合赊淑,一個(gè)是偶數(shù)集合爵政,另外一個(gè)是奇數(shù)集合。我們首先想到的就是過(guò)濾操作陶缺,通過(guò)兩次過(guò)濾操作钾挟,很簡(jiǎn)單的就完成了我們的需求。
但是這樣操作起來(lái)有問(wèn)題组哩。首先等龙,為了執(zhí)行兩次過(guò)濾操作,需要有兩個(gè)流伶贰。其次蛛砰,如果過(guò)濾操作復(fù)雜,每個(gè)流上都要執(zhí)行這樣的操作黍衙, 代碼也會(huì)變得冗余泥畅。
這里我們就不得不說(shuō)Collectors庫(kù)中的partitioningBy
方法,它接受一個(gè)流琅翻,并將其分成兩部分:使用Predicate對(duì)象位仁,指定條件并判斷一個(gè)元素應(yīng)該屬于哪個(gè)部分,并根據(jù)布爾值返回一個(gè)Map到列表方椎。因此對(duì)于key為true所對(duì)應(yīng)的List中的元素聂抢,滿(mǎn)足Predicate對(duì)象中指定的條件;同樣棠众,key為false所對(duì)應(yīng)的List中的元素琳疏,不滿(mǎn)足Predicate對(duì)象中指定的條件。
這樣闸拿,使用·partitioningBy·空盼,我們就可以將數(shù)字的Stream分解成奇數(shù)集合和偶數(shù)集合了。
Map<Boolean, List<Integer>> collectParti = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
.collect(Collectors.partitioningBy(it -> it % 2 == 0));
System.out.println("collectParti : " + collectParti);
而groupingBy
是一種更自然的分割數(shù)據(jù)操作新荤,與將數(shù)據(jù)分成true和false兩部分不同揽趾,groupingBy
可以使用任意值對(duì)數(shù)據(jù)分組。groupingBy
接受一個(gè)分類(lèi)函數(shù)Function苛骨,用來(lái)對(duì)數(shù)據(jù)分組篱瞎。
根據(jù)數(shù)字除以3的余數(shù)進(jìn)行分組:
Map<Integer, List<Integer>> collectParti2 = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
.collect(Collectors.groupingBy(t -> t % 3));
System.out.println("collectParti2 : " + collectParti2);
5.4 字符串處理
有時(shí)候,我們將Stream的元素(String類(lèi)型)最后生成一組字符串痒芝。比如在Stream.of("1", "2", "3", "4")
中奔缠,將Stream格式化成“1,2吼野,3校哎,4”。
如果不使用Stream,我們可以通過(guò)for循環(huán)迭代實(shí)現(xiàn)闷哆。
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
StringBuilder sb = new StringBuilder();
for (Integer it : list) {
if (sb.length() > 0) {
sb.append(",");
}
sb.append(it);
}
System.out.println(sb.toString());
// 打印結(jié)果
// 1,2,3,4
在Java 1.8中腰奋,我們可以使用Stream來(lái)實(shí)現(xiàn)。這里我們將使用 Collectors.joining
收集Stream中的值抱怔,該方法可以方便地將Stream得到一個(gè)字符串劣坊。joining函數(shù)接受三個(gè)參數(shù),分別表示允(用以分隔元素)屈留、前綴和后綴局冰。
String strJoin = Stream.of("1", "2", "3", "4").collect(Collectors.joining(",", "[", "]"));
System.out.println("strJoin: " + strJoin);
// 打印結(jié)果
// strJoin: [1,2,3,4]
5.5 組合Collector
前面,我們已經(jīng)了解到Collector的強(qiáng)大灌危,而且非常的實(shí)用康二。如果將他們組合起來(lái),是不是更厲害呢勇蝙?看前面舉過(guò)的例子沫勿,在數(shù)據(jù)分組時(shí),我們是得到的分組后的數(shù)據(jù)列表 collectGroup : {false=[1, 2, 3], true=[4]}
味混。如果我們的要求更高點(diǎn)产雹,我們不需要分組后的列表,只要得到分組后列表的個(gè)數(shù)就好了翁锡。
這時(shí)候蔓挖,很多人下意識(shí)的都會(huì)想到,便利Map就好了馆衔,然后使用list.size()
,就可以輕松的得到各個(gè)分組的列表個(gè)數(shù)时甚。
// 分割數(shù)據(jù)塊
Map<Boolean, List<Integer>> collectParti = Stream.of(1, 2, 3, 4)
.collect(Collectors.partitioningBy(it -> it % 2 == 0));
Map<Boolean, Integer> mapSize = new HashMap<>();
collectParti.entrySet().forEach(entry -> mapSize.put(entry.getKey(), entry.getValue().size()));
System.out.println("mapSize : " + mapSize);
// 打印結(jié)果
// mapSize : {false=2, true=2}
// 在partitioningBy方法中,有這么一個(gè)變形:
Map<Boolean, Long> partiCount = Stream.of(1, 2, 3, 4)
.collect(Collectors.partitioningBy(it -> it.intValue() % 2 == 0, Collectors.counting()));
System.out.println("partiCount: " + partiCount);
// 打印結(jié)果
// partiCount: {false=2, true=2}
在partitioningBy
方法中哈踱,我們不僅傳遞了條件函數(shù),同時(shí)傳入了第二個(gè)收集器梨熙,用以收集最終結(jié)果的一個(gè)子集开镣,這些收集器叫作下游收集器。收集器是生成最終結(jié)果的一劑配方咽扇,下游收集器則是生成部分結(jié)果的配方邪财,主收集器中會(huì)用到下游收集器。這種組合使用收集器的方式质欲, 使得它們?cè)?Stream 類(lèi)庫(kù)中的作用更加強(qiáng)大树埠。
那些為基本類(lèi)型特殊定制的函數(shù),如averagingInt
嘶伟、summarizingLong
等怎憋,事實(shí)上和調(diào)用特殊Stream上的方法是等價(jià)的,加上它們是為了將它們當(dāng)作下游收集器來(lái)使用的。
5.6 源碼分析
Collectors.toList
工廠方法返回一個(gè)收集器绊袋,它會(huì)把流中的所有元素收集成一個(gè) List毕匀。使用廣泛而且寫(xiě)起來(lái)比較直觀,通過(guò)仔細(xì)研究這個(gè)收集器是怎么實(shí)現(xiàn)的癌别,可以很好地了解 Collector 接口是怎么定義的皂岔,以及它的方法所返回的函數(shù)在內(nèi)部是如何為collect 方法所用的。
首先讓我們?cè)谙旅娴牧斜碇锌纯?Collector 接口的定義展姐,它列出了接口的簽名以及聲明的五個(gè)方法躁垛。
public interface Collector<T, A, R> {
Supplier<A> supplier();
BiConsumer<A, T> accumulator();
Function<A, R> finisher();
BinaryOperator<A> combiner();
Set<Characteristics> characteristics();
}
Collector<T, A, R>
接受三個(gè)泛型參數(shù),對(duì)可變減少操作的數(shù)據(jù)類(lèi)型作相應(yīng)限制:
T:表示流中每個(gè)元素的類(lèi)型圾笨。
A:表示中間結(jié)果容器的類(lèi)型教馆。
R:表示最終返回的結(jié)果類(lèi)型。
Collector接口聲明了4個(gè)函數(shù)墅拭,這四個(gè)函數(shù)一起協(xié)調(diào)執(zhí)行以將元素目累積到可變結(jié)果容器中活玲,并且可以選擇地對(duì)結(jié)果進(jìn)行最終的變換.
- Supplier<A> supplier(): 創(chuàng)建新的結(jié)果結(jié)
- BiConsumer<A, T> accumulator(): 將元素添加到結(jié)果容器
- BinaryOperator<A> combiner(): 將兩個(gè)結(jié)果容器合并為一個(gè)結(jié)果容器
- Function<A, R> finisher(): 對(duì)結(jié)果容器作相應(yīng)的變換
- Set<Characteristics> characteristics():對(duì)Collector聲明相關(guān)約束
Collector中定義了一個(gè)枚舉類(lèi)Characteristics
,有三個(gè)枚舉值谍婉,理解這三個(gè)值的含義對(duì)于我們自己編寫(xiě)正確的收集器也是至關(guān)重要的舒憾。
- Characteristics.CONCURRENT:表示中間結(jié)果只有一個(gè),即使在并行流的情況下穗熬。所以只有在并行流且收集器不具備CONCURRENT特性時(shí)镀迂,combiner方法返回的Lambda表達(dá)式才會(huì)執(zhí)行(中間結(jié)果容器只有一個(gè)就無(wú)需合并)。
- Characteristics.UNORDER:表示流中的元素?zé)o序唤蔗。
- Characteristics.IDENTITY_FINISH:表示中間結(jié)果容器類(lèi)型與最終結(jié)果類(lèi)型一致探遵,此時(shí)finiser方法不會(huì)被調(diào)用。
注:如果一個(gè)容器僅聲明CONCURRENT屬性妓柜,而不是UNORDERED屬性箱季,那么該容器僅僅支持無(wú)序的Stream在多線程中執(zhí)行。
現(xiàn)在我們可以一個(gè)個(gè)來(lái)分析 Collector 接口聲明的五個(gè)方法了棍掐。通過(guò)分析藏雏,你會(huì)注意到,前四個(gè)方法都會(huì)返回一個(gè)會(huì)被 collect
方法調(diào)用的函數(shù)作煌,而第五個(gè)方法 characteristics
則提供了一系列特征掘殴,也就是一個(gè)提示列表,告訴 collect
方法在執(zhí)行歸約操作的時(shí)候可以應(yīng)用哪些優(yōu)化(比如并行化)粟誓。
5.6.1 建立新的結(jié)果容器: supplier 方法
supplier
方法必須返回一個(gè)結(jié)果為空的 Supplier 奏寨,也就是一個(gè)無(wú)參數(shù)函數(shù),在調(diào)用時(shí)它會(huì)創(chuàng)建一個(gè)空的累加器實(shí)例鹰服,供數(shù)據(jù)收集過(guò)程使用病瞳。很明顯揽咕,對(duì)于將累加器本身作為結(jié)果返回的收集器,比如 ToListCollector
仍源,在對(duì)空流執(zhí)行操作的時(shí)候心褐,這個(gè)空的累加器也代表了收集過(guò)程的結(jié)果。在 ToListCollector
中笼踩, supplier 返回一個(gè)空的 List 逗爹,如下所示:
@Override
public Supplier<List<T>> supplier() {
return () -> new ArrayList<>();
}
請(qǐng)注意你也可以只傳遞一個(gè)構(gòu)造函數(shù)引用:
@Override
public Supplier<List<T>> supplier() {
return ArrayList::new;
}
5.6.2 將元素添加到結(jié)果容器: accumulator 方法
accumulator
方法會(huì)返回執(zhí)行歸約操作的函數(shù)。當(dāng)遍歷到流中第n個(gè)元素時(shí)嚎于,這個(gè)函數(shù)執(zhí)行時(shí)會(huì)有兩個(gè)參數(shù):保存歸約結(jié)果的累加器(已收集了流中的前 n-1 個(gè)項(xiàng)目)掘而,還有第n個(gè)元素本身。該函數(shù)將返回void 于购,因?yàn)槔奂悠魇窃桓屡鬯春瘮?shù)的執(zhí)行改變了它的內(nèi)部狀態(tài)以體現(xiàn)遍歷的元素的效果。對(duì)于ToListCollector
肋僧,這個(gè)函數(shù)僅僅會(huì)把當(dāng)前項(xiàng)目添加至已經(jīng)遍歷過(guò)的項(xiàng)目的列表:
@Override
public BiConsumer<List<T>, T> accumulator() {
return (list, item) -> list.add(item);
}
你也可以使用方法引用斑胜,這會(huì)更為簡(jiǎn)潔:
@Override
public BiConsumer<List<T>, T> accumulator() {
return List::add;
}
5.6.3 對(duì)結(jié)果容器應(yīng)用最終轉(zhuǎn)換: finisher 方法
在遍歷完流后, finisher
方法必須返回在累積過(guò)程的最后要調(diào)用的一個(gè)函數(shù)嫌吠,以便將累加器對(duì)象轉(zhuǎn)換為整個(gè)集合操作的最終結(jié)果止潘。通常,就像 ToListCollector
的情況一樣辫诅,累加器對(duì)象恰好符合預(yù)期的最終結(jié)果凭戴,因此無(wú)需進(jìn)行轉(zhuǎn)換。所以 finisher
方法只需返回 identity
函數(shù):
@Override
public Function<List<T>, List<T>> finisher() {
return Function.identity();
}
這三個(gè)方法已經(jīng)足以對(duì)流進(jìn)行循序規(guī)約炕矮。實(shí)踐中的實(shí)現(xiàn)細(xì)節(jié)可能還要復(fù)雜一點(diǎn)么夫,一方面是應(yīng)為流的延遲性質(zhì),可能在collect操作之前還需完成其他中間操作的流水線肤视,另一方面則是理論上可能要進(jìn)行并行規(guī)約档痪。
5.6.4 合并兩個(gè)結(jié)果容器: combiner 方法
四個(gè)方法中的最后一個(gè)——combiner
方法會(huì)返回一個(gè)供歸約操作的使用函數(shù),它定義了對(duì)流的各個(gè)子部分進(jìn)行并行處理時(shí)邢滑,各個(gè)子部分歸約所得的累加器要如何合并腐螟。對(duì)于toList
而言,這個(gè)方法的實(shí)現(xiàn)非常簡(jiǎn)單殊鞭,只要把從流的第二個(gè)部分收集到的項(xiàng)目列表加到遍歷第一部分時(shí)得到的列表后面就行了:
@Override
public BinaryOperator<List<T>> combiner() {
return (list1, list2) -> {
list1.addAll(list2);
return list1;
};
}
有了這第四個(gè)方法,就可以對(duì)流進(jìn)行并行歸約了尼桶。它會(huì)用到Java7中引入的分支/合并框架和Spliterator抽象操灿。
5.6.5 characteristics 方法
最后一個(gè)方法—— characteristics
會(huì)返回一個(gè)不可變的 Characteristics 集合,它定義了收集器的行為——尤其是關(guān)于流是否可以并行歸約泵督,以及可以使用哪些優(yōu)化的提示趾盐。Characteristics 是一個(gè)包含三個(gè)項(xiàng)目的枚舉。
- UNORDERED ——?dú)w約結(jié)果不受流中項(xiàng)目的遍歷和累積順序的影響。
- CONCURRENT ——
accumulator
函數(shù)可以從多個(gè)線程同時(shí)調(diào)用救鲤,且該收集器可以并行歸約流久窟。如果收集器沒(méi)有標(biāo)為 UNORDERED ,那它僅在用于無(wú)序數(shù)據(jù)源時(shí)才可以并行歸約本缠。 - IDENTITY_FINISH ——這表明完成器方法返回的函數(shù)是一個(gè)恒等函數(shù)斥扛,可以跳過(guò)。這種情況下丹锹,累加器對(duì)象將會(huì)直接用作歸約過(guò)程的最終結(jié)果稀颁。這也意味著,將累加器 A 不加檢查地轉(zhuǎn)換為結(jié)果 R 是安全的楣黍。
我們迄今開(kāi)發(fā)的 ToListCollector
是 IDENTITY_FINISH
的匾灶,因?yàn)橛脕?lái)累積流中元素的List 已經(jīng)是我們要的最終結(jié)果,用不著進(jìn)一步轉(zhuǎn)換了租漂,但它并不是 UNORDERED
阶女,因?yàn)橛迷谟行蛄魃系臅r(shí)候,我們還是希望順序能夠保留在得到的 List 中哩治。最后秃踩,它是 CONCURRENT
的,但我們剛才說(shuō)過(guò)了锚扎,僅僅在背后的數(shù)據(jù)源無(wú)序時(shí)才會(huì)并行處理吞瞪。
5.6.6 融會(huì)貫通
前一小節(jié)中談到的五個(gè)方法足夠我們開(kāi)發(fā)自己的 ToListCollector
了。你可以把它們都融合起來(lái)驾孔,如下面的代碼清單所示芍秆。
public class ToListCollector<T> implements Collector<T, List<T>, List<T>> {
@Override
public Supplier<List<T>> supplier() {
return ArrayList::new;
}
@Override
public BiConsumer<List<T>, T> accumulator() {
return List::add;
}
@Override
public BinaryOperator<List<T>> combiner() {
return (list1, list2) -> {
list1.addAll(list2);
return list1;
};
}
@Override
public Function<List<T>, List<T>> finisher() {
return Function.identity();
}
@Override
public Set<Characteristics> characteristics() {
return Collections.unmodifiableSet(EnumSet.of(Characteristics.IDENTITY_FINISH, Characteristics.CONCURRENT));
}
}
請(qǐng)注意,這個(gè)是實(shí)現(xiàn)與Collections.toList()
方法并不完全相同翠勉,但區(qū)別僅僅是一些小的優(yōu)化妖啥。
對(duì)于 IDENTITY_FINISH
的收集操作,還有一種方法可以得到同樣的結(jié)果而無(wú)需從頭實(shí)現(xiàn)新的 Collectors
接口对碌。 Stream 有一個(gè)重載的 collect
方法可以接受另外三個(gè)函數(shù)—— supplier
荆虱、accumulator
和 combiner
,其語(yǔ)義和 Collector 接口的相應(yīng)方法返回的函數(shù)完全相同朽们。所以比如說(shuō)怀读,我們可以像下面這樣把菜肴流中的項(xiàng)目收集到一個(gè) List 中:
List<Dish> dishes = menuStream.collect(ArrayList::new, List::add, List::addAll);
我們認(rèn)為,這第二種形式雖然比前一個(gè)寫(xiě)法更為緊湊和簡(jiǎn)潔骑脱,卻不那么易讀菜枷。此外,以恰當(dāng)?shù)念?lèi)來(lái)實(shí)現(xiàn)自己的自定義收集器有助于重用并可避免代碼重復(fù)叁丧。另外值得注意的是啤誊,這第二個(gè)collect
方法不能傳遞任何 Characteristics
岳瞭,所以它永遠(yuǎn)都是一個(gè) IDENTITY_FINISH
和CONCURRENT
但并非 UNORDERED
的收集器。