這可能是史上最好的 Java8 新特性 Stream 流教程

本文翻譯自 https://winterbe.com/posts/2014/07/31/java8-stream-tutorial-examples/

作者: @Winterbe

歡迎關(guān)注個(gè)人微信公眾號(hào): 小哈學(xué)Java

個(gè)人網(wǎng)站: https://www.exception.site/java8/java8-stream-tutorial

java8 新特性 stream 流教程

Stream 流可以說(shuō)是 Java8 新特性中用起來(lái)最爽的一個(gè)功能了豺撑,有了它娶耍,從此操作集合告別繁瑣的 for 循環(huán)倒槐。但是還有很多小伙伴對(duì) Stream 流不是很了解。今天就通過(guò)這篇 @Winterbe 的譯文,一起深入了解下如何使用它吧。

目錄

一、Stream 流是如何工作的?

二漩怎、不同類(lèi)型的 Stream 流

三、Stream 流的處理順序

四嗦嗡、中間操作順序這么重要勋锤?

五、數(shù)據(jù)流復(fù)用問(wèn)題

六侥祭、高級(jí)操作

  • 6.1 Collect
  • 6.2 FlatMap
  • 6.3 Reduce

七叁执、并行流

八、結(jié)語(yǔ)


當(dāng)我第一次閱讀 Java8 中的 Stream API 時(shí)卑硫,說(shuō)實(shí)話徒恋,我非常困惑,因?yàn)樗拿致?tīng)起來(lái)與 Java I0 框架中的 InputStreamOutputStream 非常類(lèi)似欢伏。但是實(shí)際上入挣,它們完全是不同的東西。

Java8 Stream 使用的是函數(shù)式編程模式硝拧,如同它的名字一樣径筏,它可以被用來(lái)對(duì)集合進(jìn)行鏈狀流式的操作葛假。

本文就將帶著你如何使用 Java 8 不同類(lèi)型的 Stream 操作。同時(shí)您還將了解流的處理順序滋恬,以及不同順序的流操作是如何影響運(yùn)行時(shí)性能的聊训。

我們還將學(xué)習(xí)終端操作 API reducecollect 以及flatMap的詳細(xì)介紹恢氯,最后我們?cè)賮?lái)深入的探討一下 Java8 并行流带斑。

注意:如果您還不熟悉 Java 8 lambda 表達(dá)式,函數(shù)式接口以及方法引用勋拟,您可以先閱讀一下小哈的另一篇譯文 《Java8 新特性教程》

接下來(lái)勋磕,就讓我們進(jìn)入正題吧!

一敢靡、Stream 流是如何工作的挂滓?

流表示包含著一系列元素的集合,我們可以對(duì)其做不同類(lèi)型的操作啸胧,用來(lái)對(duì)這些元素執(zhí)行計(jì)算赶站。聽(tīng)上去可能有點(diǎn)拗口,讓我們用代碼說(shuō)話:

List<String> myList =
    Arrays.asList("a1", "a2", "b1", "c2", "c1");

myList
    .stream() // 創(chuàng)建流
    .filter(s -> s.startsWith("c")) // 執(zhí)行過(guò)濾纺念,過(guò)濾出以 c 為前綴的字符串
    .map(String::toUpperCase) // 轉(zhuǎn)換成大寫(xiě)
    .sorted() // 排序
    .forEach(System.out::println); // for 循環(huán)打印

// C1
// C2

我們可以對(duì)流進(jìn)行中間操作或者終端操作贝椿。小伙伴們可能會(huì)疑問(wèn)?什么是中間操作柠辞?什么又是終端操作团秽?

Stream中間操作主胧,終端操作
  • :中間操作會(huì)再次返回一個(gè)流叭首,所以,我們可以鏈接多個(gè)中間操作踪栋,注意這里是不用加分號(hào)的焙格。上圖中的filter 過(guò)濾,map 對(duì)象轉(zhuǎn)換夷都,sorted 排序眷唉,就屬于中間操作。
  • :終端操作是對(duì)流操作的一個(gè)結(jié)束動(dòng)作囤官,一般返回 void 或者一個(gè)非流的結(jié)果冬阳。上圖中的 forEach循環(huán) 就是一個(gè)終止操作。

看完上面的操作党饮,感覺(jué)是不是很像一個(gè)流水線式操作呢肝陪。

實(shí)際上,大部分流操作都支持 lambda 表達(dá)式作為參數(shù)刑顺,正確理解氯窍,應(yīng)該說(shuō)是接受一個(gè)函數(shù)式接口的實(shí)現(xiàn)作為參數(shù)饲常。

二、不同類(lèi)型的 Stream 流

我們可以從各種數(shù)據(jù)源中創(chuàng)建 Stream 流狼讨,其中以 Collection 集合最為常見(jiàn)贝淤。如 ListSet 均支持 stream() 方法來(lái)創(chuàng)建順序流或者是并行流。

并行流是通過(guò)多線程的方式來(lái)執(zhí)行的政供,它能夠充分發(fā)揮多核 CPU 的優(yōu)勢(shì)來(lái)提升性能播聪。本文在最后再來(lái)介紹并行流,我們先討論順序流:

Arrays.asList("a1", "a2", "a3")
    .stream() // 創(chuàng)建流
    .findFirst() // 找到第一個(gè)元素
    .ifPresent(System.out::println);  // 如果存在布隔,即輸出

// a1

在集合上調(diào)用stream()方法會(huì)返回一個(gè)普通的 Stream 流犬耻。但是, 您大可不必刻意地創(chuàng)建一個(gè)集合,再通過(guò)集合來(lái)獲取 Stream 流执泰,您還可以通過(guò)如下這種方式:

Stream.of("a1", "a2", "a3")
    .findFirst()
    .ifPresent(System.out::println);  // a1

例如上面這樣枕磁,我們可以通過(guò) Stream.of() 從一堆對(duì)象中創(chuàng)建 Stream 流。

除了常規(guī)對(duì)象流之外术吝,Java 8還附帶了一些特殊類(lèi)型的流计济,用于處理原始數(shù)據(jù)類(lèi)型intlong以及double排苍。說(shuō)道這里沦寂,你可能已經(jīng)猜到了它們就是IntStreamLongStream還有DoubleStream淘衙。

其中传藏,IntStreams.range()方法還可以被用來(lái)取代常規(guī)的 for 循環(huán), 如下所示:

IntStream.range(1, 4)
    .forEach(System.out::println); // 相當(dāng)于 for (int i = 1; i < 4; i++) {}

// 1
// 2
// 3

上面這些原始類(lèi)型流的工作方式與常規(guī)對(duì)象流基本是一樣的,但還是略微存在一些區(qū)別:

  • 原始類(lèi)型流使用其獨(dú)有的函數(shù)式接口彤守,例如IntFunction代替Function毯侦,IntPredicate代替Predicate

  • 原始類(lèi)型流支持額外的終端聚合操作具垫,sum()以及average()侈离,如下所示:

Arrays.stream(new int[] {1, 2, 3})
    .map(n -> 2 * n + 1) // 對(duì)數(shù)值中的每個(gè)對(duì)象執(zhí)行 2*n + 1 操作
    .average() // 求平均值
    .ifPresent(System.out::println);  // 如果值不為空,則輸出
// 5.0

但是筝蚕,偶爾我們也有這種需求卦碾,需要將常規(guī)對(duì)象流轉(zhuǎn)換為原始類(lèi)型流,這個(gè)時(shí)候起宽,中間操作 mapToInt()洲胖,mapToLong() 以及mapToDouble就派上用場(chǎng)了:

Stream.of("a1", "a2", "a3")
    .map(s -> s.substring(1)) // 對(duì)每個(gè)字符串元素從下標(biāo)1位置開(kāi)始截取
    .mapToInt(Integer::parseInt) // 轉(zhuǎn)成 int 基礎(chǔ)類(lèi)型類(lèi)型流
    .max() // 取最大值
    .ifPresent(System.out::println);  // 不為空則輸出

// 3

如果說(shuō),您需要將原始類(lèi)型流裝換成對(duì)象流坯沪,您可以使用 mapToObj()來(lái)達(dá)到目的:

IntStream.range(1, 4)
    .mapToObj(i -> "a" + i) // for 循環(huán) 1->4, 拼接前綴 a
    .forEach(System.out::println); // for 循環(huán)打印

// a1
// a2
// a3

下面是一個(gè)組合示例绿映,我們將雙精度流首先轉(zhuǎn)換成 int 類(lèi)型流,然后再將其裝換成對(duì)象流:

Stream.of(1.0, 2.0, 3.0)
    .mapToInt(Double::intValue) // double 類(lèi)型轉(zhuǎn) int
    .mapToObj(i -> "a" + i) // 對(duì)值拼接前綴 a
    .forEach(System.out::println); // for 循環(huán)打印

// a1
// a2
// a3

三屏箍、Stream 流的處理順序

上小節(jié)中绘梦,我們已經(jīng)學(xué)會(huì)了如何創(chuàng)建不同類(lèi)型的 Stream 流橘忱,接下來(lái)我們?cè)偕钊肓私庀聰?shù)據(jù)流的執(zhí)行順序。

在討論處理順序之前卸奉,您需要明確一點(diǎn)钝诚,那就是中間操作的有個(gè)重要特性 —— 延遲性。觀察下面這個(gè)沒(méi)有終端操作的示例代碼:

Stream.of("d2", "a2", "b1", "b3", "c")
    .filter(s -> {
        System.out.println("filter: " + s);
        return true;
    });

執(zhí)行此代碼段時(shí)榄棵,您可能會(huì)認(rèn)為凝颇,將依次打印 "d2", "a2", "b1", "b3", "c" 元素。然而當(dāng)你實(shí)際去執(zhí)行的時(shí)候疹鳄,它不會(huì)打印任何內(nèi)容拧略。

為什么呢?

原因是:當(dāng)且僅當(dāng)存在終端操作時(shí)瘪弓,中間操作操作才會(huì)被執(zhí)行垫蛆。

是不是不信?接下來(lái)腺怯,對(duì)上面的代碼添加 forEach終端操作:

Stream.of("d2", "a2", "b1", "b3", "c")
    .filter(s -> {
        System.out.println("filter: " + s);
        return true;
    })
    .forEach(s -> System.out.println("forEach: " + s));

再次執(zhí)行袱饭,我們會(huì)看到輸出如下:

filter:  d2
forEach: d2
filter:  a2
forEach: a2
filter:  b1
forEach: b1
filter:  b3
forEach: b3
filter:  c
forEach: c

輸出的順序可能會(huì)讓你很驚訝!你腦海里肯定會(huì)想呛占,應(yīng)該是先將所有 filter 前綴的字符串打印出來(lái)濒募,接著才會(huì)打印 forEach 前綴的字符串技俐。

事實(shí)上,輸出的結(jié)果卻是隨著鏈條垂直移動(dòng)的纹份。比如說(shuō)抵恋,當(dāng) Stream 開(kāi)始處理 d2 元素時(shí)丁侄,它實(shí)際上會(huì)在執(zhí)行完 filter 操作后徐绑,再執(zhí)行 forEach 操作伐割,接著才會(huì)處理第二個(gè)元素。

是不是很神奇坠狡?為什么要設(shè)計(jì)成這樣呢继找?

原因是出于性能的考慮。這樣設(shè)計(jì)可以減少對(duì)每個(gè)元素的實(shí)際操作數(shù)逃沿,看完下面代碼你就明白了:

Stream.of("d2", "a2", "b1", "b3", "c")
    .map(s -> {
        System.out.println("map: " + s);
        return s.toUpperCase(); // 轉(zhuǎn)大寫(xiě)
    })
    .anyMatch(s -> {
        System.out.println("anyMatch: " + s);
        return s.startsWith("A"); // 過(guò)濾出以 A 為前綴的元素
    });

// map:      d2
// anyMatch: D2
// map:      a2
// anyMatch: A2

終端操作 anyMatch()表示任何一個(gè)元素以 A 為前綴,返回為 true幻锁,就停止循環(huán)凯亮。所以它會(huì)從 d2 開(kāi)始匹配,接著循環(huán)到 a2 的時(shí)候哄尔,返回為 true 假消,于是停止循環(huán)。

由于數(shù)據(jù)流的鏈?zhǔn)秸{(diào)用是垂直執(zhí)行的岭接,map這里只需要執(zhí)行兩次富拗。相對(duì)于水平執(zhí)行來(lái)說(shuō)臼予,map會(huì)執(zhí)行盡可能少的次數(shù),而不是把所有元素都 map 轉(zhuǎn)換一遍啃沪。

四粘拾、中間操作順序這么重要?

下面的例子由兩個(gè)中間操作mapfilter创千,以及一個(gè)終端操作forEach組成缰雇。讓我們?cè)賮?lái)看看這些操作是如何執(zhí)行的:

Stream.of("d2", "a2", "b1", "b3", "c")
    .map(s -> {
        System.out.println("map: " + s);
        return s.toUpperCase(); // 轉(zhuǎn)大寫(xiě)
    })
    .filter(s -> {
        System.out.println("filter: " + s);
        return s.startsWith("A"); // 過(guò)濾出以 A 為前綴的元素
    })
    .forEach(s -> System.out.println("forEach: " + s)); // for 循環(huán)輸出

// map:     d2
// filter:  D2
// map:     a2
// filter:  A2
// forEach: A2
// map:     b1
// filter:  B1
// map:     b3
// filter:  B3
// map:     c
// filter:  C

學(xué)習(xí)了上面一小節(jié),您應(yīng)該已經(jīng)知道了追驴,mapfilter會(huì)對(duì)集合中的每個(gè)字符串調(diào)用五次械哟,而forEach卻只會(huì)調(diào)用一次,因?yàn)橹挥?"a2" 滿足過(guò)濾條件殿雪。

如果我們改變中間操作的順序暇咆,將filter移動(dòng)到鏈頭的最開(kāi)始,就可以大大減少實(shí)際的執(zhí)行次數(shù):

Stream.of("d2", "a2", "b1", "b3", "c")
    .filter(s -> {
        System.out.println("filter: " + s)
        return s.startsWith("a"); // 過(guò)濾出以 a 為前綴的元素
    })
    .map(s -> {
        System.out.println("map: " + s);
        return s.toUpperCase(); // 轉(zhuǎn)大寫(xiě)
    })
    .forEach(s -> System.out.println("forEach: " + s)); // for 循環(huán)輸出

// filter:  d2
// filter:  a2
// map:     a2
// forEach: A2
// filter:  b1
// filter:  b3
// filter:  c

現(xiàn)在丙曙,map僅僅只需調(diào)用一次糯崎,性能得到了提升,這種小技巧對(duì)于流中存在大量元素來(lái)說(shuō)河泳,是非常很有用的沃呢。

接下來(lái),讓我們對(duì)上面的代碼再添加一個(gè)中間操作sorted

Stream.of("d2", "a2", "b1", "b3", "c")
    .sorted((s1, s2) -> {
        System.out.printf("sort: %s; %s\n", s1, s2);
        return s1.compareTo(s2); // 排序
    })
    .filter(s -> {
        System.out.println("filter: " + s);
        return s.startsWith("a"); // 過(guò)濾出以 a 為前綴的元素
    })
    .map(s -> {
        System.out.println("map: " + s);
        return s.toUpperCase(); // 轉(zhuǎn)大寫(xiě)
    })
    .forEach(s -> System.out.println("forEach: " + s)); // for 循環(huán)輸出

sorted 是一個(gè)有狀態(tài)的操作拆挥,因?yàn)樗枰谔幚淼倪^(guò)程中薄霜,保存狀態(tài)以對(duì)集合中的元素進(jìn)行排序。

執(zhí)行上面代碼纸兔,輸出如下:

sort:    a2; d2
sort:    b1; a2
sort:    b1; d2
sort:    b1; a2
sort:    b3; b1
sort:    b3; d2
sort:    c; b3
sort:    c; d2
filter:  a2
map:     a2
forEach: A2
filter:  b1
filter:  b3
filter:  c
filter:  d2

咦咦咦惰瓜?這次怎么又不是垂直執(zhí)行了。你需要知道的是汉矿,sorted是水平執(zhí)行的崎坊。因此,在這種情況下洲拇,sorted會(huì)對(duì)集合中的元素組合調(diào)用八次奈揍。這里,我們也可以利用上面說(shuō)道的優(yōu)化技巧赋续,將 filter 過(guò)濾中間操作移動(dòng)到開(kāi)頭部分:

Stream.of("d2", "a2", "b1", "b3", "c")
    .filter(s -> {
        System.out.println("filter: " + s);
        return s.startsWith("a");
    })
    .sorted((s1, s2) -> {
        System.out.printf("sort: %s; %s\n", s1, s2);
        return s1.compareTo(s2);
    })
    .map(s -> {
        System.out.println("map: " + s);
        return s.toUpperCase();
    })
    .forEach(s -> System.out.println("forEach: " + s));

// filter:  d2
// filter:  a2
// filter:  b1
// filter:  b3
// filter:  c
// map:     a2
// forEach: A2

從上面的輸出中男翰,我們看到了 sorted從未被調(diào)用過(guò),因?yàn)榻?jīng)過(guò)filter過(guò)后的元素已經(jīng)減少到只有一個(gè)纽乱,這種情況下蛾绎,是不用執(zhí)行排序操作的。因此性能被大大提高了。

五租冠、數(shù)據(jù)流復(fù)用問(wèn)題

Java8 Stream 流是不能被復(fù)用的鹏倘,一旦你調(diào)用任何終端操作,流就會(huì)關(guān)閉:

Stream<String> stream =
    Stream.of("d2", "a2", "b1", "b3", "c")
        .filter(s -> s.startsWith("a"));

stream.anyMatch(s -> true);    // ok
stream.noneMatch(s -> true);   // exception

當(dāng)我們對(duì) stream 調(diào)用了 anyMatch 終端操作以后顽爹,流即關(guān)閉了纤泵,再調(diào)用 noneMatch 就會(huì)拋出異常:

java.lang.IllegalStateException: stream has already been operated upon or closed
    at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:229)
    at java.util.stream.ReferencePipeline.noneMatch(ReferencePipeline.java:459)
    at com.winterbe.java8.Streams5.test7(Streams5.java:38)
    at com.winterbe.java8.Streams5.main(Streams5.java:28)

為了克服這個(gè)限制,我們必須為我們想要執(zhí)行的每個(gè)終端操作創(chuàng)建一個(gè)新的流鏈话原,例如夕吻,我們可以通過(guò) Supplier 來(lái)包裝一下流,通過(guò) get() 方法來(lái)構(gòu)建一個(gè)新的 Stream 流繁仁,如下所示:

Supplier<Stream<String>> streamSupplier =
    () -> Stream.of("d2", "a2", "b1", "b3", "c")
            .filter(s -> s.startsWith("a"));

streamSupplier.get().anyMatch(s -> true);   // ok
streamSupplier.get().noneMatch(s -> true);  // ok

通過(guò)構(gòu)造一個(gè)新的流涉馅,來(lái)避開(kāi)流不能被復(fù)用的限制, 這也是取巧的一種方式。

六黄虱、高級(jí)操作

Streams 支持的操作很豐富稚矿,除了上面介紹的這些比較常用的中間操作,如filtermap(參見(jiàn)Stream Javadoc)外捻浦。還有一些更復(fù)雜的操作晤揣,如collectflatMap以及reduce朱灿。接下來(lái)昧识,就讓我們學(xué)習(xí)一下:

本小節(jié)中的大多數(shù)代碼示例均會(huì)使用以下 List<Person>進(jìn)行演示:

class Person {
    String name;
    int age;

    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return name;
    }
}

// 構(gòu)建一個(gè) Person 集合
List<Person> persons =
    Arrays.asList(
        new Person("Max", 18),
        new Person("Peter", 23),
        new Person("Pamela", 23),
        new Person("David", 12));

6.1 Collect

collect 是一個(gè)非常有用的終端操作,它可以將流中的元素轉(zhuǎn)變成另外一個(gè)不同的對(duì)象盗扒,例如一個(gè)List跪楞,SetMap。collect 接受入?yún)?code>Collector(收集器)侣灶,它由四個(gè)不同的操作組成:供應(yīng)器(supplier)甸祭、累加器(accumulator)、組合器(combiner)和終止器(finisher)褥影。

這些都是個(gè)啥池户?別慌,看上去非常復(fù)雜的樣子凡怎,但好在大多數(shù)情況下校焦,您并不需要自己去實(shí)現(xiàn)收集器。因?yàn)?Java 8通過(guò)Collectors類(lèi)內(nèi)置了各種常用的收集器栅贴,你直接拿來(lái)用就行了斟湃。

讓我們先從一個(gè)非常常見(jiàn)的用例開(kāi)始:

List<Person> filtered =
    persons
        .stream() // 構(gòu)建流
        .filter(p -> p.name.startsWith("P")) // 過(guò)濾出名字以 P 開(kāi)頭的
        .collect(Collectors.toList()); // 生成一個(gè)新的 List

System.out.println(filtered);    // [Peter, Pamela]

你也看到了,從流中構(gòu)造一個(gè) List 異常簡(jiǎn)單檐薯。如果說(shuō)你需要構(gòu)造一個(gè) Set 集合,只需要使用Collectors.toSet()就可以了。

接下來(lái)這個(gè)示例坛缕,將會(huì)按年齡對(duì)所有人進(jìn)行分組:

Map<Integer, List<Person>> personsByAge = persons
    .stream()
    .collect(Collectors.groupingBy(p -> p.age)); // 以年齡為 key,進(jìn)行分組

personsByAge
    .forEach((age, p) -> System.out.format("age %s: %s\n", age, p));

// age 18: [Max]
// age 23: [Peter, Pamela]
// age 12: [David]

除了上面這些操作墓猎。您還可以在流上執(zhí)行聚合操作,例如赚楚,計(jì)算所有人的平均年齡:

Double averageAge = persons
    .stream()
    .collect(Collectors.averagingInt(p -> p.age)); // 聚合出平均年齡

System.out.println(averageAge);     // 19.0

如果您還想得到一個(gè)更全面的統(tǒng)計(jì)信息毙沾,摘要收集器可以返回一個(gè)特殊的內(nèi)置統(tǒng)計(jì)對(duì)象。通過(guò)它宠页,我們可以簡(jiǎn)單地計(jì)算出最小年齡左胞、最大年齡、平均年齡举户、總和以及總數(shù)量烤宙。

IntSummaryStatistics ageSummary =
    persons
        .stream()
        .collect(Collectors.summarizingInt(p -> p.age)); // 生成摘要統(tǒng)計(jì)

System.out.println(ageSummary);
// IntSummaryStatistics{count=4, sum=76, min=12, average=19.000000, max=23}

下一個(gè)這個(gè)示例,可以將所有人名連接成一個(gè)字符串:

String phrase = persons
    .stream()
    .filter(p -> p.age >= 18) // 過(guò)濾出年齡大于等于18的
    .map(p -> p.name) // 提取名字
    .collect(Collectors.joining(" and ", "In Germany ", " are of legal age.")); // 以 In Germany 開(kāi)頭俭嘁,and 連接各元素躺枕,再以 are of legal age. 結(jié)束

System.out.println(phrase);
// In Germany Max and Peter and Pamela are of legal age.

連接收集器的入?yún)⒔邮芊指舴约翱蛇x的前綴以及后綴供填。

對(duì)于如何將流轉(zhuǎn)換為 Map集合拐云,我們必須指定 Map 的鍵和值。這里需要注意近她,Map 的鍵必須是唯一的叉瘩,否則會(huì)拋出IllegalStateException 異常。

你可以選擇傳遞一個(gè)合并函數(shù)作為額外的參數(shù)來(lái)避免發(fā)生這個(gè)異常:

Map<Integer, String> map = persons
    .stream()
    .collect(Collectors.toMap(
        p -> p.age,
        p -> p.name,
        (name1, name2) -> name1 + ";" + name2)); // 對(duì)于同樣 key 的粘捎,將值拼接

System.out.println(map);
// {18=Max, 23=Peter;Pamela, 12=David}

既然我們已經(jīng)知道了這些強(qiáng)大的內(nèi)置收集器薇缅,接下來(lái)就讓我們嘗試構(gòu)建自定義收集器吧。

比如說(shuō)晌端,我們希望將流中的所有人轉(zhuǎn)換成一個(gè)字符串捅暴,包含所有大寫(xiě)的名稱(chēng),并以|分割咧纠。為了達(dá)到這種效果蓬痒,我們需要通過(guò)Collector.of()創(chuàng)建一個(gè)新的收集器。同時(shí)漆羔,我們還需要傳入收集器的四個(gè)組成部分:供應(yīng)器梧奢、累加器、組合器和終止器演痒。

Collector<Person, StringJoiner, String> personNameCollector =
    Collector.of(
        () -> new StringJoiner(" | "),          // supplier 供應(yīng)器
        (j, p) -> j.add(p.name.toUpperCase()),  // accumulator 累加器
        (j1, j2) -> j1.merge(j2),               // combiner 組合器
        StringJoiner::toString);                // finisher 終止器

String names = persons
    .stream()
    .collect(personNameCollector); // 傳入自定義的收集器

System.out.println(names);  // MAX | PETER | PAMELA | DAVID

由于Java 中的字符串是 final 類(lèi)型的亲轨,我們需要借助輔助類(lèi)StringJoiner,來(lái)幫我們構(gòu)造字符串鸟顺。

最開(kāi)始供應(yīng)器使用分隔符構(gòu)造了一個(gè)StringJointer惦蚊。

累加器用于將每個(gè)人的人名轉(zhuǎn)大寫(xiě)器虾,然后加到StringJointer中。

組合器將兩個(gè)StringJointer合并為一個(gè)蹦锋。

最終兆沙,終結(jié)器從StringJointer構(gòu)造出預(yù)期的字符串。

6.2 FlatMap

上面我們已經(jīng)學(xué)會(huì)了如通過(guò)map操作, 將流中的對(duì)象轉(zhuǎn)換為另一種類(lèi)型莉掂。但是葛圃,Map只能將每個(gè)對(duì)象映射到另一個(gè)對(duì)象。

如果說(shuō)憎妙,我們想要將一個(gè)對(duì)象轉(zhuǎn)換為多個(gè)其他對(duì)象或者根本不做轉(zhuǎn)換操作呢库正?這個(gè)時(shí)候,flatMap就派上用場(chǎng)了厘唾。

FlatMap 能夠?qū)⒘鞯拿總€(gè)元素, 轉(zhuǎn)換為其他對(duì)象的流褥符。因此,每個(gè)對(duì)象可以被轉(zhuǎn)換為零個(gè)阅嘶,一個(gè)或多個(gè)其他對(duì)象属瓣,并以流的方式返回。之后讯柔,這些流的內(nèi)容會(huì)被放入flatMap返回的流中抡蛙。

在學(xué)習(xí)如何實(shí)際操作flatMap之前,我們先新建兩個(gè)類(lèi)魂迄,用來(lái)測(cè)試:

class Foo {
    String name;
    List<Bar> bars = new ArrayList<>();

    Foo(String name) {
        this.name = name;
    }
}

class Bar {
    String name;

    Bar(String name) {
        this.name = name;
    }
}

接下來(lái)粗截,通過(guò)我們上面學(xué)習(xí)到的流知識(shí),來(lái)實(shí)例化一些對(duì)象:

List<Foo> foos = new ArrayList<>();

// 創(chuàng)建 foos 集合
IntStream
    .range(1, 4)
    .forEach(i -> foos.add(new Foo("Foo" + i)));

// 創(chuàng)建 bars 集合
foos.forEach(f ->
    IntStream
        .range(1, 4)
        .forEach(i -> f.bars.add(new Bar("Bar" + i + " <- " + f.name))));

我們創(chuàng)建了包含三個(gè)foo的集合捣炬,每個(gè)foo中又包含三個(gè) bar熊昌。

flatMap 的入?yún)⒔邮芤粋€(gè)返回對(duì)象流的函數(shù)。為了處理每個(gè)foo中的bar湿酸,我們需要傳入相應(yīng) stream 流:

foos.stream()
    .flatMap(f -> f.bars.stream())
    .forEach(b -> System.out.println(b.name));

// Bar1 <- Foo1
// Bar2 <- Foo1
// Bar3 <- Foo1
// Bar1 <- Foo2
// Bar2 <- Foo2
// Bar3 <- Foo2
// Bar1 <- Foo3
// Bar2 <- Foo3
// Bar3 <- Foo3

如上所示婿屹,我們已成功將三個(gè) foo對(duì)象的流轉(zhuǎn)換為九個(gè)bar對(duì)象的流。

最后推溃,上面的這段代碼可以簡(jiǎn)化為單一的流式操作:

IntStream.range(1, 4)
    .mapToObj(i -> new Foo("Foo" + i))
    .peek(f -> IntStream.range(1, 4)
        .mapToObj(i -> new Bar("Bar" + i + " <- " f.name))
        .forEach(f.bars::add))
    .flatMap(f -> f.bars.stream())
    .forEach(b -> System.out.println(b.name));

flatMap也可用于Java8引入的Optional類(lèi)昂利。OptionalflatMap操作返回一個(gè)Optional或其他類(lèi)型的對(duì)象。所以它可以用于避免繁瑣的null檢查铁坎。

接下來(lái)蜂奸,讓我們創(chuàng)建層次更深的對(duì)象:

class Outer {
    Nested nested;
}

class Nested {
    Inner inner;
}

class Inner {
    String foo;
}

為了處理從 Outer 對(duì)象中獲取最底層的 foo 字符串,你需要添加多個(gè)null檢查來(lái)避免可能發(fā)生的NullPointerException硬萍,如下所示:

Outer outer = new Outer();
if (outer != null && outer.nested != null && outer.nested.inner != null) {
    System.out.println(outer.nested.inner.foo);
}

我們還可以使用OptionalflatMap操作扩所,來(lái)完成上述相同功能的判斷,且更加優(yōu)雅:

Optional.of(new Outer())
    .flatMap(o -> Optional.ofNullable(o.nested))
    .flatMap(n -> Optional.ofNullable(n.inner))
    .flatMap(i -> Optional.ofNullable(i.foo))
    .ifPresent(System.out::println);

如果不為空的話朴乖,每個(gè)flatMap的調(diào)用都會(huì)返回預(yù)期對(duì)象的Optional包裝祖屏,否則返回為nullOptional包裝類(lèi)助赞。

筆者補(bǔ)充:關(guān)于 Optional 可參見(jiàn)我另一篇譯文《Java8 新特性如何防止空指針異常》

6.3 Reduce

規(guī)約操作可以將流的所有元素組合成一個(gè)結(jié)果赐劣。Java 8 支持三種不同的reduce方法嫉拐。第一種將流中的元素規(guī)約成流中的一個(gè)元素哩都。

讓我們看看如何使用這種方法魁兼,來(lái)篩選出年齡最大的那個(gè)人:

persons
    .stream()
    .reduce((p1, p2) -> p1.age > p2.age ? p1 : p2)
    .ifPresent(System.out::println);    // Pamela

reduce方法接受BinaryOperator積累函數(shù)。該函數(shù)實(shí)際上是兩個(gè)操作數(shù)類(lèi)型相同的BiFunction漠嵌。BiFunction功能和Function一樣咐汞,但是它接受兩個(gè)參數(shù)。示例代碼中儒鹿,我們比較兩個(gè)人的年齡化撕,來(lái)返回年齡較大的人。

第二種reduce方法接受標(biāo)識(shí)值和BinaryOperator累加器约炎。此方法可用于構(gòu)造一個(gè)新的 Person植阴,其中包含來(lái)自流中所有其他人的聚合名稱(chēng)和年齡:

Person result =
    persons
        .stream()
        .reduce(new Person("", 0), (p1, p2) -> {
            p1.age += p2.age;
            p1.name += p2.name;
            return p1;
        });

System.out.format("name=%s; age=%s", result.name, result.age);
// name=MaxPeterPamelaDavid; age=76

第三種reduce方法接受三個(gè)參數(shù):標(biāo)識(shí)值,BiFunction累加器和類(lèi)型的組合器函數(shù)BinaryOperator圾浅。由于初始值的類(lèi)型不一定為Person掠手,我們可以使用這個(gè)歸約函數(shù)來(lái)計(jì)算所有人的年齡總和:

Integer ageSum = persons
    .stream()
    .reduce(0, (sum, p) -> sum += p.age, (sum1, sum2) -> sum1 + sum2);

System.out.println(ageSum);  // 76

結(jié)果為76,但是內(nèi)部究竟發(fā)生了什么呢狸捕?讓我們?cè)俅蛴∫恍┱{(diào)試日志:

Integer ageSum = persons
    .stream()
    .reduce(0,
        (sum, p) -> {
            System.out.format("accumulator: sum=%s; person=%s\n", sum, p);
            return sum += p.age;
        },
        (sum1, sum2) -> {
            System.out.format("combiner: sum1=%s; sum2=%s\n", sum1, sum2);
            return sum1 + sum2;
        });

// accumulator: sum=0; person=Max
// accumulator: sum=18; person=Peter
// accumulator: sum=41; person=Pamela
// accumulator: sum=64; person=David

你可以看到喷鸽,累加器函數(shù)完成了所有工作。它首先使用初始值0和第一個(gè)人年齡相加灸拍。接下來(lái)的三步中sum會(huì)持續(xù)增加做祝,直到76。

等等鸡岗?好像哪里不太對(duì)混槐!組合器從來(lái)都沒(méi)有調(diào)用過(guò)啊轩性?

我們以并行流的方式運(yùn)行上面的代碼声登,看看日志輸出:

Integer ageSum = persons
    .parallelStream()
    .reduce(0,
        (sum, p) -> {
            System.out.format("accumulator: sum=%s; person=%s\n", sum, p);
            return sum += p.age;
        },
        (sum1, sum2) -> {
            System.out.format("combiner: sum1=%s; sum2=%s\n", sum1, sum2);
            return sum1 + sum2;
        });

// accumulator: sum=0; person=Pamela
// accumulator: sum=0; person=David
// accumulator: sum=0; person=Max
// accumulator: sum=0; person=Peter
// combiner: sum1=18; sum2=23
// combiner: sum1=23; sum2=12
// combiner: sum1=41; sum2=35

并行流的執(zhí)行方式完全不同。這里組合器被調(diào)用了炮姨。實(shí)際上捌刮,由于累加器被并行調(diào)用,組合器需要被用于計(jì)算部分累加值的總和舒岸。

讓我們?cè)谙乱徽律钊胩接懖⑿辛鳌?/p>

七绅作、并行流

流是可以并行執(zhí)行的,當(dāng)流中存在大量元素時(shí)蛾派,可以顯著提升性能俄认。并行流底層使用的ForkJoinPool, 它由ForkJoinPool.commonPool()方法提供个少。底層線程池的大小最多為五個(gè) - 具體取決于 CPU 可用核心數(shù):

ForkJoinPool commonPool = ForkJoinPool.commonPool();
System.out.println(commonPool.getParallelism());    // 3

在我的機(jī)器上,公共池初始化默認(rèn)值為 3眯杏。你也可以通過(guò)設(shè)置以下JVM參數(shù)可以減小或增加此值:

-Djava.util.concurrent.ForkJoinPool.common.parallelism=5

集合支持parallelStream()方法來(lái)創(chuàng)建元素的并行流夜焦。或者你可以在已存在的數(shù)據(jù)流上調(diào)用中間方法parallel()岂贩,將串行流轉(zhuǎn)換為并行流茫经,這也是可以的。

為了詳細(xì)了解并行流的執(zhí)行行為萎津,我們?cè)谙旅娴氖纠a中卸伞,打印當(dāng)前線程的信息:

Arrays.asList("a1", "a2", "b1", "c2", "c1")
    .parallelStream()
    .filter(s -> {
        System.out.format("filter: %s [%s]\n",
            s, Thread.currentThread().getName());
        return true;
    })
    .map(s -> {
        System.out.format("map: %s [%s]\n",
            s, Thread.currentThread().getName());
        return s.toUpperCase();
    })
    .forEach(s -> System.out.format("forEach: %s [%s]\n",
        s, Thread.currentThread().getName()));

通過(guò)日志輸出,我們可以對(duì)哪個(gè)線程被用于執(zhí)行流式操作锉屈,有個(gè)更深入的理解:

filter:  b1 [main]
filter:  a2 [ForkJoinPool.commonPool-worker-1]
map:     a2 [ForkJoinPool.commonPool-worker-1]
filter:  c2 [ForkJoinPool.commonPool-worker-3]
map:     c2 [ForkJoinPool.commonPool-worker-3]
filter:  c1 [ForkJoinPool.commonPool-worker-2]
map:     c1 [ForkJoinPool.commonPool-worker-2]
forEach: C2 [ForkJoinPool.commonPool-worker-3]
forEach: A2 [ForkJoinPool.commonPool-worker-1]
map:     b1 [main]
forEach: B1 [main]
filter:  a1 [ForkJoinPool.commonPool-worker-3]
map:     a1 [ForkJoinPool.commonPool-worker-3]
forEach: A1 [ForkJoinPool.commonPool-worker-3]
forEach: C1 [ForkJoinPool.commonPool-worker-2]

如您所見(jiàn)荤傲,并行流使用了所有的ForkJoinPool中的可用線程來(lái)執(zhí)行流式操作。在持續(xù)的運(yùn)行中颈渊,輸出結(jié)果可能有所不同遂黍,因?yàn)樗褂玫奶囟ň€程是非特定的。

讓我們通過(guò)添加中間操作sort來(lái)擴(kuò)展上面示例:

Arrays.asList("a1", "a2", "b1", "c2", "c1")
    .parallelStream()
    .filter(s -> {
        System.out.format("filter: %s [%s]\n",
            s, Thread.currentThread().getName());
        return true;
    })
    .map(s -> {
        System.out.format("map: %s [%s]\n",
            s, Thread.currentThread().getName());
        return s.toUpperCase();
    })
    .sorted((s1, s2) -> {
        System.out.format("sort: %s <> %s [%s]\n",
            s1, s2, Thread.currentThread().getName());
        return s1.compareTo(s2);
    })
    .forEach(s -> System.out.format("forEach: %s [%s]\n",
        s, Thread.currentThread().getName()));

運(yùn)行代碼俊嗽,輸出結(jié)果看上去有些奇怪:

filter:  c2 [ForkJoinPool.commonPool-worker-3]
filter:  c1 [ForkJoinPool.commonPool-worker-2]
map:     c1 [ForkJoinPool.commonPool-worker-2]
filter:  a2 [ForkJoinPool.commonPool-worker-1]
map:     a2 [ForkJoinPool.commonPool-worker-1]
filter:  b1 [main]
map:     b1 [main]
filter:  a1 [ForkJoinPool.commonPool-worker-2]
map:     a1 [ForkJoinPool.commonPool-worker-2]
map:     c2 [ForkJoinPool.commonPool-worker-3]
sort:    A2 <> A1 [main]
sort:    B1 <> A2 [main]
sort:    C2 <> B1 [main]
sort:    C1 <> C2 [main]
sort:    C1 <> B1 [main]
sort:    C1 <> C2 [main]
forEach: A1 [ForkJoinPool.commonPool-worker-1]
forEach: C2 [ForkJoinPool.commonPool-worker-3]
forEach: B1 [main]
forEach: A2 [ForkJoinPool.commonPool-worker-2]
forEach: C1 [ForkJoinPool.commonPool-worker-1]

貌似sort只在主線程上串行執(zhí)行雾家。但是實(shí)際上,并行流中的sort在底層使用了Java8中新的方法Arrays.parallelSort()乌询。如 javadoc官方文檔解釋的榜贴,這個(gè)方法會(huì)按照數(shù)據(jù)長(zhǎng)度來(lái)決定以串行方式,或者以并行的方式來(lái)執(zhí)行妹田。

如果指定數(shù)據(jù)的長(zhǎng)度小于最小數(shù)值唬党,它則使用相應(yīng)的Arrays.sort方法來(lái)進(jìn)行排序。

回到上小節(jié) reduce的例子鬼佣。我們已經(jīng)發(fā)現(xiàn)了組合器函數(shù)只在并行流中調(diào)用驶拱,而不不會(huì)在串行流中被調(diào)用。

讓我們來(lái)實(shí)際觀察一下涉及到哪個(gè)線程:

List<Person> persons = Arrays.asList(
    new Person("Max", 18),
    new Person("Peter", 23),
    new Person("Pamela", 23),
    new Person("David", 12));

persons
    .parallelStream()
    .reduce(0,
        (sum, p) -> {
            System.out.format("accumulator: sum=%s; person=%s [%s]\n",
                sum, p, Thread.currentThread().getName());
            return sum += p.age;
        },
        (sum1, sum2) -> {
            System.out.format("combiner: sum1=%s; sum2=%s [%s]\n",
                sum1, sum2, Thread.currentThread().getName());
            return sum1 + sum2;
        });

通過(guò)控制臺(tái)日志輸出晶衷,累加器和組合器均在所有可用的線程上并行執(zhí)行:

accumulator: sum=0; person=Pamela; [main]
accumulator: sum=0; person=Max;    [ForkJoinPool.commonPool-worker-3]
accumulator: sum=0; person=David;  [ForkJoinPool.commonPool-worker-2]
accumulator: sum=0; person=Peter;  [ForkJoinPool.commonPool-worker-1]
combiner:    sum1=18; sum2=23;     [ForkJoinPool.commonPool-worker-1]
combiner:    sum1=23; sum2=12;     [ForkJoinPool.commonPool-worker-2]
combiner:    sum1=41; sum2=35;     [ForkJoinPool.commonPool-worker-2]

總之蓝纲,你需要記住的是,并行流對(duì)含有大量元素的數(shù)據(jù)流提升性能極大晌纫。但是你也需要記住并行流的一些操作税迷,例如reducecollect操作,需要額外的計(jì)算(如組合操作)锹漱,這在串行執(zhí)行時(shí)是并不需要箭养。

此外,我們也了解了哥牍,所有并行流操作都共享相同的 JVM 相關(guān)的公共ForkJoinPool毕泌。所以你可能需要避免寫(xiě)出一些又慢又卡的流式操作喝检,這很有可能會(huì)拖慢你應(yīng)用中,嚴(yán)重依賴(lài)并行流的其它部分代碼的性能撼泛。

八挠说、結(jié)語(yǔ)

Java8 Stream 流編程指南到這里就結(jié)束了。如果您有興趣了解更多有關(guān) Java 8 Stream 流的相關(guān)信息愿题,我建議您使用 Stream Javadoc 閱讀官方文檔损俭。如果您想了解有關(guān)底層機(jī)制的更多信息,您也可以閱讀 Martin Fowlers 關(guān)于 Collection Pipelines 的文章抠忘。

最后撩炊,祝您學(xué)習(xí)愉快!

贈(zèng)送 | 面試&學(xué)習(xí)福利資源

最近在網(wǎng)上發(fā)現(xiàn)一個(gè)不錯(cuò)的 PDF 資源《Java 核心面試知識(shí).pdf》分享給大家崎脉,不光是面試,學(xué)習(xí)伯顶,你都值得擁有G糇啤!祭衩!

獲取方式: 關(guān)注微信公眾號(hào): 小哈學(xué)Java, 后臺(tái)回復(fù)"資源"灶体,既可免費(fèi)無(wú)套路獲取資源鏈接,下面是目錄以及部分截圖:

福利資源截圖
福利資源截圖
福利資源截圖
福利資源截圖
福利資源截圖
福利資源截圖
福利資源截圖

重要的事情說(shuō)兩遍掐暮,獲取方式: 關(guān)注微信公眾號(hào): 小哈學(xué)Java, 后臺(tái)回復(fù)"資源"蝎抽,既可免費(fèi)無(wú)套路獲取資源鏈接 !B房恕樟结!

歡迎關(guān)注微信公眾號(hào): 小哈學(xué)Java

小哈學(xué)Java
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市精算,隨后出現(xiàn)的幾起案子瓢宦,更是在濱河造成了極大的恐慌,老刑警劉巖灰羽,帶你破解...
    沈念sama閱讀 211,123評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件驮履,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡廉嚼,警方通過(guò)查閱死者的電腦和手機(jī)玫镐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)怠噪,“玉大人恐似,你說(shuō)我怎么就攤上這事〗⒒妫” “怎么了蹂喻?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,723評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵葱椭,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我口四,道長(zhǎng)贴唇,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,357評(píng)論 1 283
  • 正文 為了忘掉前任滋早,我火速辦了婚禮峦耘,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘赤嚼。我一直安慰自己旷赖,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,412評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布更卒。 她就那樣靜靜地躺著等孵,像睡著了一般。 火紅的嫁衣襯著肌膚如雪蹂空。 梳的紋絲不亂的頭發(fā)上俯萌,一...
    開(kāi)封第一講書(shū)人閱讀 49,760評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音上枕,去河邊找鬼咐熙。 笑死,一個(gè)胖子當(dāng)著我的面吹牛辨萍,可吹牛的內(nèi)容都是我干的棋恼。 我是一名探鬼主播,決...
    沈念sama閱讀 38,904評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼锈玉,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼爪飘!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起嘲玫,我...
    開(kāi)封第一講書(shū)人閱讀 37,672評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤悦施,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后去团,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體抡诞,經(jīng)...
    沈念sama閱讀 44,118評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,456評(píng)論 2 325
  • 正文 我和宋清朗相戀三年土陪,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了昼汗。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,599評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡鬼雀,死狀恐怖顷窒,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤鞋吉,帶...
    沈念sama閱讀 34,264評(píng)論 4 328
  • 正文 年R本政府宣布鸦做,位于F島的核電站,受9級(jí)特大地震影響谓着,放射性物質(zhì)發(fā)生泄漏泼诱。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,857評(píng)論 3 312
  • 文/蒙蒙 一赊锚、第九天 我趴在偏房一處隱蔽的房頂上張望治筒。 院中可真熱鬧,春花似錦舷蒲、人聲如沸耸袜。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,731評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)堤框。三九已至,卻和暖如春欠拾,著一層夾襖步出監(jiān)牢的瞬間胰锌,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,956評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工藐窄, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人酬土。 一個(gè)月前我還...
    沈念sama閱讀 46,286評(píng)論 2 360
  • 正文 我出身青樓荆忍,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親撤缴。 傳聞我的和親對(duì)象是個(gè)殘疾皇子刹枉,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,465評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容

  • 本文采用實(shí)例驅(qū)動(dòng)的方式,對(duì)JAVA8的stream API進(jìn)行一個(gè)深入的介紹屈呕。雖然JAVA8中的stream AP...
    浮梁翁閱讀 25,723評(píng)論 3 50
  • 歡迎交流java8新特性系列文章:http://www.reibang.com/nb/27231419 . [...
    DoubleBin閱讀 15,057評(píng)論 0 41
  • 1微宝、Stream簡(jiǎn)介 Stream作為Java 8的一大亮點(diǎn),是對(duì)集合(Collection)虎眨、數(shù)組對(duì)象功能的增強(qiáng)...
    Albert_Yu閱讀 6,910評(píng)論 1 21
  • Java 8 數(shù)據(jù)流教程 原文:Java 8 Stream Tutorial 譯者:飛龍 協(xié)議:CC BY-NC-...
    布客飛龍閱讀 939評(píng)論 1 46
  • 對(duì)于Java開(kāi)發(fā)者來(lái)說(shuō)蟋软,Java8的版本顯然是一個(gè)具有里程碑意義的版本,蘊(yùn)含了許多令人激動(dòng)的新特性嗽桩,如果能利用好這...
    jackcooper閱讀 1,020評(píng)論 0 6