JAVA基礎(chǔ)之Stream

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)用 shapesiterator()方法進(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è)管道铝量,如下圖所示:

1.png

流的操作類(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拄轻,filtermapToInt 為 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:

IntStreamLongStream辉浦、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))。

limitskip 對(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 操作 limitskip 的作用下,管道中 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@816f27d, 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告希、limitskip 甚至 distinct 來(lái)減少元素?cái)?shù)量后烧给,再排序燕偶,這能幫助程序明顯縮短執(zhí)行時(shí)間。

優(yōu)化:排序前進(jìn)行 limitskip

    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@6ce253f1, stream.StreamDWPerson@53d8d10a]

當(dāng)然础嫡,這種優(yōu)化是有 business logic 上的局限性的:即不要求排序后再取值指么。

3.8 min/max/distinct

minmax 的功能也可以通過(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

iteratereduce 操作很像翰舌,接受一個(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)該使用這些流订咸。
  • 有些操作本身在并行流上的性能就比順序流差。特別是limitfindFirst等依賴(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)換成值。例如堰燎,maxByminBy允許用戶(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ā)的 ToListCollectorIDENTITY_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 荆虱、accumulatorcombiner ,其語(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_FINISHCONCURRENT 但并非 UNORDERED 的收集器。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末蚊锹,一起剝皮案震驚了整個(gè)濱河市瞳筏,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌牡昆,老刑警劉巖姚炕,帶你破解...
    沈念sama閱讀 221,635評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異迁杨,居然都是意外死亡钻心,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)铅协,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)捷沸,“玉大人,你說(shuō)我怎么就攤上這事狐史⊙鞲” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,083評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵骏全,是天一觀的道長(zhǎng)苍柏。 經(jīng)常有香客問(wèn)我,道長(zhǎng)姜贡,這世上最難降的妖魔是什么试吁? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,640評(píng)論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮楼咳,結(jié)果婚禮上熄捍,老公的妹妹穿的比我還像新娘。我一直安慰自己母怜,他們只是感情好余耽,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,640評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著苹熏,像睡著了一般碟贾。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上轨域,一...
    開(kāi)封第一講書(shū)人閱讀 52,262評(píng)論 1 308
  • 那天袱耽,我揣著相機(jī)與錄音,去河邊找鬼干发。 笑死朱巨,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的铐然。 我是一名探鬼主播蔬崩,決...
    沈念sama閱讀 40,833評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼搀暑!你這毒婦竟也來(lái)了沥阳?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,736評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤自点,失蹤者是張志新(化名)和其女友劉穎桐罕,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體桂敛,經(jīng)...
    沈念sama閱讀 46,280評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡功炮,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,369評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了术唬。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片薪伏。...
    茶點(diǎn)故事閱讀 40,503評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖粗仓,靈堂內(nèi)的尸體忽然破棺而出嫁怀,到底是詐尸還是另有隱情,我是刑警寧澤借浊,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布塘淑,位于F島的核電站,受9級(jí)特大地震影響蚂斤,放射性物質(zhì)發(fā)生泄漏存捺。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,870評(píng)論 3 333
  • 文/蒙蒙 一曙蒸、第九天 我趴在偏房一處隱蔽的房頂上張望捌治。 院中可真熱鬧,春花似錦逸爵、人聲如沸具滴。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,340評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)构韵。三九已至,卻和暖如春趋艘,著一層夾襖步出監(jiān)牢的瞬間疲恢,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,460評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工瓷胧, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留显拳,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,909評(píng)論 3 376
  • 正文 我出身青樓搓萧,卻偏偏與公主長(zhǎng)得像杂数,于是被迫代替她去往敵國(guó)和親宛畦。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,512評(píng)論 2 359

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

  • 1揍移、Stream簡(jiǎn)介 Stream作為Java 8的一大亮點(diǎn)次和,是對(duì)集合(Collection)、數(shù)組對(duì)象功能的增強(qiáng)...
    Albert_Yu閱讀 7,180評(píng)論 1 21
  • 轉(zhuǎn)自: Java 8 中的 Streams API 詳解 為什么需要 Stream Stream 作為 Java ...
    普度眾生的面癱青年閱讀 2,920評(píng)論 0 11
  • Java8 in action 沒(méi)有共享的可變數(shù)據(jù)那伐,將方法和函數(shù)即代碼傳遞給其他方法的能力就是我們平常所說(shuō)的函數(shù)式...
    鐵牛很鐵閱讀 1,235評(píng)論 1 2
  • 第一章 為什么要關(guān)心Java 8 使用Stream庫(kù)來(lái)選擇最佳低級(jí)執(zhí)行機(jī)制可以避免使用Synchronized(同...
    謝隨安閱讀 1,494評(píng)論 0 4
  • Stream API Stream是Java8中處理集合的關(guān)鍵抽象概念踏施,它可以指定你希望對(duì)集合進(jìn)行的操作,但是將執(zhí)...
    sprainkle閱讀 2,244評(píng)論 0 3