Java 8中的Lambda 和 Stream (from Effective Java 第三版)

42.Lambda 優(yōu)先于匿名類

??在之前的做法中(Historically)联四,使用單個(gè)抽象方法的接口(或很少的抽象類【只有一個(gè)抽象方法的抽象類數(shù)量比較少】)被用作函數(shù)類型悯恍。它們的實(shí)例稱為函數(shù)對(duì)象蛮拔,代表一個(gè)函數(shù)或一種行為谒出。自 JDK 1.1 于 1997 年發(fā)布以來(lái)宦搬,創(chuàng)建函數(shù)對(duì)象的主要方法是匿名類(第 24 項(xiàng))。下面的這個(gè)代碼片段毡代,用于按長(zhǎng)度順序?qū)ψ址斜磉M(jìn)行排序阅羹,使用匿名類創(chuàng)建排序的比較函數(shù)(強(qiáng)制排序順序):

// Anonymous class instance as a function object - obsolete!
Collections.sort(words, new Comparator<String>() {
    public int compare(String s1, String s2) {
        return Integer.compare(s1.length(), s2.length());
    }
});

??匿名類適用于需要經(jīng)典功能的面向?qū)ο蟮脑O(shè)計(jì)模式勺疼,特別是策略模式[Gamma95]。Comparator 接口表示用于排序的抽象策略; 上面的匿名類是排序字符串的具體策略捏鱼。然而执庐,匿名類的冗長(zhǎng)使得 Java 中的函數(shù)式編程成為一個(gè)沒(méi)有吸引力的前景。

??在 Java 8 中导梆,該語(yǔ)言正式成為這樣一種概念轨淌,即使用單一抽象方法的接口是特殊的,值得特別對(duì)待看尼。這些接口現(xiàn)在稱為功能接口递鹉,該語(yǔ)言允許你使用 lambda 表達(dá)式或簡(jiǎn)稱 lambdas 創(chuàng)建這些接口的實(shí)例。Lambdas 在功能上與匿名類相似藏斩,但更加簡(jiǎn)潔躏结。以下是上面的代碼片段如何將匿名類替換為 lambda。樣板消失了狰域,行為很明顯:

// Lambda expression as function object (replaces anonymous class)
Collections.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length()));

??請(qǐng)注意媳拴,lambda(Comparator <String>)的類型,其參數(shù)(s1 和 s2兆览,兩個(gè) String)及其返回值(int)的類型不在代碼中屈溉。編譯器使用稱為類型推斷的過(guò)程從上下文中推導(dǎo)出這些類型。在某些情況下抬探,編譯器將無(wú)法確定類型子巾,你必須指定它們。

類型推斷的規(guī)則很復(fù)雜:它們占據(jù)了 JLS 的整個(gè)章節(jié) [JLS驶睦,18]砰左。很少有程序員詳細(xì)了解這些規(guī)則匿醒,但這沒(méi)關(guān)系场航。
省略所有 lambda 參數(shù)的類型,除非它們的存在使您的程序更清晰廉羔。
如果編譯器生成錯(cuò)誤溉痢,告訴你無(wú)法推斷 lambda 參數(shù)的類型,請(qǐng)指定它憋他。有時(shí)你可能必須轉(zhuǎn)換返回值或整個(gè) lambda 表達(dá)式孩饼,但這種情況很少見(jiàn)。

??關(guān)于類型推斷竹挡,應(yīng)該添加一個(gè)警告镀娶。第 26 項(xiàng)告訴你不要使用原始類型,第 29 項(xiàng)告訴你支持泛型類型揪罕,第 30 項(xiàng)告訴你支持泛型方法梯码。當(dāng)你使用 lambdas 時(shí)宝泵,這個(gè)建議是非常重要的,因?yàn)榫幾g器獲得了從泛型的執(zhí)行類型推斷出的大多數(shù)類型信息轩娶。如果你不提供此信息儿奶,編譯器將無(wú)法進(jìn)行類型推斷,你必須在 lambdas 中手動(dòng)指定類型鳄抒,這將大大增加它們的詳細(xì)程度【也就是代碼量】闯捎。舉例來(lái)說(shuō),如果變量詞被聲明為原始類型 List 而不是參數(shù)化類型 List <String>许溅,那么上面的代碼片段將無(wú)法編譯瓤鼻。

??順便提一下,如果使用比較器構(gòu)造方法代替 lambda贤重,則片段中的比較器可以更簡(jiǎn)潔(第 14. 43 項(xiàng)):

Collections.sort(words, comparingInt(String::length));

??實(shí)際上娱仔,通過(guò)利用 Java 8 中添加到 List 接口的 sort 方法,可以使代碼段更短:

words.sort(comparingInt(String::length));

??將 lambda 添加到語(yǔ)言中使得使用函數(shù)對(duì)象變得切實(shí)可行游桩。例如牲迫,請(qǐng)考慮第 34 項(xiàng)中的 Operation 枚舉類型。因?yàn)槊總€(gè)枚舉對(duì)其 apply 方法需要不同的行為借卧,所以我們使用特定于常量的類主體并覆蓋每個(gè)枚舉常量中的 apply 方法盹憎。為了讓你有清晰的記憶,這里是代碼:

// Enum type with constant-specific class bodies & data (Item 34)
public enum Operation {
    PLUS("+") {
        public double apply(double x, double y) { return x + y; }
    },
    MINUS("-") {
        public double apply(double x, double y) { return x - y; }
    },
    TIMES("*") {
        public double apply(double x, double y) { return x * y; }
    },
    DIVIDE("/") {
        public double apply(double x, double y) { return x / y; }
    };
    private final String symbol;
    Operation(String symbol) { this.symbol = symbol; }
    @Override
    public String toString() { return symbol; }
    public abstract double apply(double x, double y);
}

??第 34 項(xiàng)說(shuō) enum 實(shí)例字段比特定于常量的類體更可取铐刘。使用前者而不是后者陪每,Lambdas 可以輕松實(shí)現(xiàn)特定于常量的行為。只需將實(shí)現(xiàn)每個(gè)枚舉常量行為的 lambda 傳遞給它的構(gòu)造函數(shù)镰吵。構(gòu)造函數(shù)將 lambda 存儲(chǔ)在實(shí)例字段中檩禾,apply 方法將調(diào)用轉(zhuǎn)發(fā)給 lambda。生成的代碼比原始版本更簡(jiǎn)單疤祭,更清晰:

// Enum with function object fields & constant-specific behavior
public enum Operation {
    PLUS ("+", (x, y) -> x + y),
    MINUS ("-", (x, y) -> x - y),
    TIMES ("*", (x, y) -> x * y),
    DIVIDE("/", (x, y) -> x / y);
    private final String symbol;
    private final DoubleBinaryOperator op;

    Operation(String symbol, DoubleBinaryOperator op) {
        this.symbol = symbol;
        this.op = op;
    }

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

    public double apply(double x, double y) {
        return op.applyAsDouble(x, y);
    }
}

??請(qǐng)注意盼产,我們使用 DoubleBinaryOperator 接口來(lái)表示枚舉常量行為的 lambdas。這是 java.util.function(第 44 項(xiàng))中許多預(yù)定義的功能接口之一勺馆。它表示一個(gè)函數(shù)戏售,它接受兩個(gè) double 參數(shù)并返回一個(gè) double 結(jié)果。

??查看基于 lambda 的 Operation 枚舉草穆,您可能會(huì)認(rèn)為特定于常量的方法體已經(jīng)過(guò)時(shí)了灌灾,但事實(shí)并非如此。跟類和方法不一樣悲柱,lambdas 缺乏名稱和文檔; 如果一個(gè)運(yùn)算過(guò)程不能自我解釋【代碼就是最好的文檔】锋喜,或超過(guò)幾行,請(qǐng)不要將它放在 lambda 中豌鸡。一行【代碼】對(duì)于 lambda 是理想的嘿般,三行【代碼】是合理的最大值轴总。如果違反此規(guī)則,可能會(huì)嚴(yán)重?fù)p害程序的可讀性博个。如果 lambda 很長(zhǎng)或難以閱讀怀樟,要么找到簡(jiǎn)化它的方法,要么重構(gòu)你的程序來(lái)取代 lambda盆佣。此外往堡,傳遞給枚舉構(gòu)造函數(shù)的參數(shù)在靜態(tài)上下文中進(jìn)行運(yùn)算。因此共耍,枚舉構(gòu)造函數(shù)中的 lambdas 無(wú)法訪問(wèn)枚舉的實(shí)例成員虑灰。如果枚舉類型具有難以理解的特定于常量的行為,無(wú)法在幾行【代碼】中實(shí)現(xiàn)痹兜,或者需要訪問(wèn)實(shí)例字段或方法穆咐,則仍然可以使用特定于常量的類主體。

??同樣字旭,你可能會(huì)認(rèn)為匿名類在 lambdas 時(shí)代已經(jīng)過(guò)時(shí)了对湃。這很接近事實(shí),但是你可以用匿名類做一些你無(wú)法用 lambdas 做的事情遗淳。Lambdas 僅限于函數(shù)接口拍柒。如果要?jiǎng)?chuàng)建抽象類的實(shí)例,可以使用匿名類屈暗,但不能使用 lambda拆讯。同樣,你可以使用匿名類來(lái)創(chuàng)建具有多個(gè)抽象方法的接口實(shí)例养叛。最后种呐,lambda 無(wú)法獲得對(duì)自身的引用。在 lambda 中弃甥,this 關(guān)鍵字引用封閉的實(shí)例爽室,這通常是你想要的。在匿名類中潘飘,this 關(guān)鍵字引用匿名類實(shí)例肮之。如果需要從其體內(nèi)【類內(nèi)部】訪問(wèn)函數(shù)對(duì)象,則必須使用匿名類卜录。【在 lambda 表達(dá)式中使用 this 關(guān)鍵字眶明,獲得的引用是 lambda 所在的實(shí)例的引用艰毒,在匿名類中使用 this 關(guān)鍵字,獲得的是當(dāng)前匿名類的實(shí)例的引用】

??Lambdas 與匿名類都具有無(wú)法在實(shí)現(xiàn)中可靠地序列化和反序列化它們的屬性【lambda 和匿名類都無(wú)法被序列化和反序列化】搜囱。因此丑瞧,你應(yīng)該很少(如果有的話)序列化 lambda(或匿名類實(shí)例)柑土。如果您有一個(gè)要進(jìn)行序列化的函數(shù)對(duì)象,例如 Comparator绊汹,請(qǐng)使用私有靜態(tài)嵌套類的實(shí)例(第 24 項(xiàng))稽屏。

??總之,從 Java 8 開始西乖,lambda 是迄今為止表示小函數(shù)對(duì)象的最佳方式狐榔。除非必須創(chuàng)建非功能接口類型的實(shí)例,否則不要對(duì)函數(shù)對(duì)象使用匿名類获雕。另外薄腻,請(qǐng)記住,lambda 使得通過(guò)使用對(duì)象來(lái)代表小函數(shù)變得如此容易届案,以至于它打開了以前在 Java 中不實(shí)用的函數(shù)式編程技術(shù)的大門庵楷。

43.方法引用優(yōu)先于 Lambda

??lambda 優(yōu)于匿名類的主要優(yōu)點(diǎn)是它們更簡(jiǎn)潔。Java 提供了一種生成函數(shù)對(duì)象的方法楣颠,它比 lambda 更簡(jiǎn)潔:方法引用尽纽。這是一個(gè)程序的代碼片段,它維護(hù)從任意 key 到 Integer 值的映射童漩。如果該值被解釋為 key 實(shí)例數(shù)的計(jì)數(shù)蜓斧,則該程序是多集實(shí)現(xiàn)。代碼段的功能是將數(shù)字 1 與 key 相關(guān)聯(lián)(如果它不在映射中)睁冬,并在 key 已存在時(shí)增加相關(guān)值:

map.merge(key, 1, (count, incr) -> count + incr);

??請(qǐng)注意挎春,此代碼使用 merge 方法,該方法已添加到 Java 8 中的 Map 接口豆拨。如果給定鍵 key 沒(méi)有映射直奋,則該方法只是插入給定的值; 如果已存在映射,則 merge 將給定的函數(shù)應(yīng)用于當(dāng)前值和給定值施禾,并使用結(jié)果覆蓋當(dāng)前值脚线。這段代碼表示 merge 方法的典型用例。

??代碼讀起來(lái)很 nice弥搞,但仍然有一些樣板【代碼】邮绿。參數(shù) count 和 incr 不會(huì)增加太多值,并且占用相當(dāng)大的空間攀例。實(shí)際上船逮,所有 lambda 告訴你的是該函數(shù)返回其兩個(gè)參數(shù)的總和。從 Java 8 開始粤铭,Integer(以及所有其他包裝的數(shù)字基本類型)提供了一個(gè)完全相同的靜態(tài)方法 sum挖胃。我們可以簡(jiǎn)單地傳遞對(duì)此方法的引用,獲得相同的結(jié)果,并且【代碼】看起來(lái)不會(huì)那么亂:

map.merge(key, 1, Integer::sum);

??方法具有的參數(shù)越多酱鸭,使用方法引用可以消除的樣板【代碼】就越多吗垮。但是,在某些 lambda 中凹髓,你選擇的參數(shù)名稱提供了有用的文檔烁登,使得 lambda 比方法引用更易讀和可維護(hù),即使 lambda 更長(zhǎng)蔚舀。

??對(duì)于一個(gè)你不能用 lambda 做的方法引用饵沧,你無(wú)能為力(有一個(gè)模糊的例外 - 如果你很好奇,請(qǐng)參閱 JLS蝗敢,9.9-2)捷泞。也就是說(shuō),方法引用通常會(huì)導(dǎo)致更短寿谴,更清晰的代碼锁右。如果 lambda 變得太長(zhǎng)或太復(fù)雜,它們也會(huì)給你一個(gè)方向(out):你可以將 lambda 中的代碼提取到一個(gè)新方法中讶泰,并用對(duì)該方法的引用替換 lambda咏瑟。你可以為該方法提供一個(gè)好名稱,并將其記錄在核心的內(nèi)容中痪署。

??如果你使用 IDE 進(jìn)行編程码泞,如果可以的話,它就會(huì)提供方法引用替換 lambda狼犯。你要經(jīng)常(并不總是)接受 IDE 提供的建議秋柄。有時(shí)候屏鳍,lambda 將比方法引用更簡(jiǎn)潔啤呼。當(dāng)方法與 lambda 屬于同一類時(shí)巷怜,這種情況最常發(fā)生。例如瓢姻,考慮這個(gè)片段祝蝠,假定它出現(xiàn)在名為 GoshThisClassNameIsHumongous 的類中:

service.execute(GoshThisClassNameIsHumongous::action);

??使用 lambda 看起來(lái)像這樣:

service.execute(() -> action());

??使用方法引用的代碼段既不比使用 lambda 的代碼段更短也更清晰,所以更喜歡后者幻碱。類似地绎狭,F(xiàn)unction 接口提供了一個(gè)通用的靜態(tài)工廠方法來(lái)返回 Identity 函數(shù) Function.identity()。它通常更短更清潔褥傍,不使用此方法儡嘶,而是編寫等效的 lambda 內(nèi)聯(lián):x -> x。

??許多方法引用會(huì)引用靜態(tài)方法摔桦,但有四種方法引用不會(huì)引用靜態(tài)方法社付。其中兩個(gè)是綁定和未綁定的實(shí)例方法引用承疲。在綁定引用中邻耕,接收對(duì)象在方法引用中指定鸥咖。綁定引用在本質(zhì)上類似于靜態(tài)引用:函數(shù)對(duì)象采用與引用方法相同的參數(shù)。在未綁定的引用中兄世,在應(yīng)用函數(shù)對(duì)象時(shí)啼辣,通過(guò)方法聲明的參數(shù)之前的附加參數(shù)指定接收對(duì)象。未綁定引用通常用作流管道(stream pipelines)(第 45 項(xiàng))中的映射和過(guò)濾功能御滩。最后鸥拧,對(duì)于類和數(shù)組,有兩種構(gòu)造函數(shù)引用削解。構(gòu)造函數(shù)引用充當(dāng)工廠對(duì)象富弦。所有五種方法參考總結(jié)在下表中:

Method Ref Type Example Lambda Equivalent
Static Integer::parseInt str -> Integer.parseInt(str)
Bound Integer::parseIntr Instant then = Instant.now();
t -> then.isAfter(t)
Unbound String::toLowerCase str -> str.toLowerCase()
Class Constructor TreeMap<K, V>::new () -> new TreeMap<K, V>
Array Constructor int[]::new len -> new int[len]

??總之,方法引用通常提供一種更簡(jiǎn)潔的 lambda 替代方案氛驮。在使用方法引用可以更簡(jiǎn)短更清晰的地方腕柜,就使用方法引用,如果無(wú)法使代碼更簡(jiǎn)短更清晰的地方就堅(jiān)持使用 lambda矫废。(Where method references are shorter and clearer, use them; where they aren’t, stick with lambdas.)

44.堅(jiān)持使用標(biāo)準(zhǔn)的函數(shù)接口

??既然 Java 有 lambda盏缤,那么編寫 API 的最佳實(shí)踐已經(jīng)發(fā)生了很大變化。例如蓖扑,模板方法模式[Gamma95]唉铜,其中子類重寫基本方法進(jìn)而具體化其超類的行為,遠(yuǎn)沒(méi)那么有吸引力÷筛埽現(xiàn)在的替代方案是提供一個(gè)靜態(tài)工廠或構(gòu)造函數(shù)潭流,它接受一個(gè)函數(shù)對(duì)象來(lái)實(shí)現(xiàn)相同的效果。更一般地說(shuō)柜去,你將編寫更多以函數(shù)對(duì)象作為參數(shù)的構(gòu)造函數(shù)和方法灰嫉。需要謹(jǐn)慎地選擇正確的功能參數(shù)類型。

??考慮 LinkedHashMap诡蜓。你可以通過(guò)重寫其受保護(hù)的 removeEldestEntry 方法將此類用作緩存熬甫,該方法每次將新 key 添加到 map 時(shí)都會(huì)調(diào)用。當(dāng)此方法返回 true 時(shí)蔓罚,map 將刪除其最舊的 entry椿肩,該 entry 將傳遞給該方法。 以下覆蓋允許 map 增長(zhǎng)到一百個(gè) entry豺谈,然后在每次添加新 key 時(shí)刪除最舊的 entry郑象,保留最近的一百個(gè) entry:

protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    return size() > 100;
}

??這種技術(shù)【實(shí)現(xiàn)方式】很好,但你可以用 lambda 做得更好茬末。如果現(xiàn)在編寫 LinkedHashMap厂榛,它將有一個(gè)帶有函數(shù)對(duì)象的靜態(tài)工廠或構(gòu)造函數(shù)盖矫。查看 removeEldestEntry 的聲明,你可能會(huì)認(rèn)為函數(shù)對(duì)象應(yīng)該采用 Map.Entry <K击奶,V>并返回一個(gè)布爾值辈双,但是不會(huì)這樣做:removeEldestEntry 方法調(diào)用 size()來(lái)獲取 map 中 entry 的數(shù)目,因?yàn)?removeEldestEntry 是 map 的實(shí)例方法柜砾。傳遞給構(gòu)造函數(shù)的函數(shù)對(duì)象不是 map 上的實(shí)例方法湃望,并且無(wú)法捕獲它,因?yàn)樵谡{(diào)用其工廠或構(gòu)造函數(shù)時(shí) map 尚不存在痰驱。因此证芭,map 必須將自身傳遞給函數(shù)對(duì)象,因此函數(shù)對(duì)象必須在輸入的地方獲得 map担映,就像獲取最老的 entry【方式】一樣【函數(shù)的形參需要傳入 map 本身以及最老的 entry】废士。如果你要聲明這樣一個(gè)功能性接口,它看起來(lái)像這樣:

// Unnecessary functional interface; use a standard one instead.
@FunctionalInterface
interface EldestEntryRemovalFunction<K,V>{
    boolean remove(Map<K,V> map, Map.Entry<K,V> eldest);
}

??此接口可以正常工作蝇完,但您不應(yīng)該使用它官硝,因?yàn)槟悴恍枰獮榱诉@個(gè)目的聲明新接口。java.util.function 包提供了大量標(biāo)準(zhǔn)功能性接口供您使用四敞。如果其中一個(gè)標(biāo)準(zhǔn)功能接口完成了這項(xiàng)工作泛源,您通常應(yīng)該優(yōu)先使用它,而不是專門構(gòu)建的功能接口忿危。這將使您的 API 學(xué)習(xí)起來(lái)更容易达箍,通過(guò)減少其概念表面積(by reducing its conceptual surface area),并將提供重要的互操作性優(yōu)勢(shì)(and will provide significant interoperability benefits)铺厨,因?yàn)樵S多標(biāo)準(zhǔn)功能性接口提供有用的默認(rèn)方法缎玫。例如,Predicate 接口提供了結(jié)合斷言(combine predicates)的方法解滓。對(duì)于 LinkedHashMap 示例赃磨,應(yīng)優(yōu)先使用標(biāo)準(zhǔn) BiPredicate <Map <K,V>洼裤,Map.Entry <K邻辉,V >>接口,而不是自定義 EldestEntryRemovalFunction 接口腮鞍。java.util.Function 中有 43 個(gè)接口值骇。不指望你記住它們,但如果你記得 6 個(gè)基本接口移国,你可以在需要時(shí)得到其余的接口吱瘩。基本接口對(duì)對(duì)象引用類型進(jìn)行操作迹缀。Operator 接口表示結(jié)果和參數(shù)類型相同的函數(shù)使碾。Predicate 接口表示一個(gè)接收一個(gè)參數(shù)并返回布爾值的函數(shù)蜜徽。Function 接口表示其參數(shù)和返回類型不同的函數(shù)。Supplier 接口表示不帶參數(shù)并返回(或“提供”)值的函數(shù)票摇。最后拘鞋,Consumer 表示一個(gè)函數(shù),它接受一個(gè)參數(shù)并且什么都不返回兄朋,本質(zhì)上消費(fèi)它的參數(shù)(essentially consuming its argument)掐禁。6 個(gè)基本功能接口總結(jié)如下:

Interface Function Signature Example
UnaryOperator<T> T apply(T t) String::toLowerCase
BinaryOperator<T> T apply(T t1, T t2) BigInteger::add
Predicate<T> boolean test(T t) Collection::isEmpty
Function<T,R> R apply(T t) Arrays::asList
Supplier<T> T get() Instant::now
Consumer<T> void accept(T t) System.out::println

??Function 接口有九個(gè)附加變體怜械,供結(jié)果類型為基本類型時(shí)使用颅和。源(source)類型和結(jié)果類型總是不同,因?yàn)閺念愋偷阶陨淼暮瘮?shù)是 UnaryOperator缕允。如果源類型和結(jié)果類型都是基本類型峡扩,則使用 SrcToResult 作為前綴 Function,例如 LongToIntFunction(六個(gè)變體)障本。如果源是基本類型并且結(jié)果是對(duì)象引用教届,則使用<Src>ToObj 作為前綴 Function,例如 DoubleToObjFunction(三個(gè)變體)驾霜。

??有三個(gè)基本功能性接口的兩個(gè)參數(shù)版本案训,使用它們是有意義的:BiPredicate <T,U>粪糙,BiFunction <T强霎,U,R>和 BiConsumer <T蓉冈,U>城舞。還有 BiFunction 變體返回三種相關(guān)的基本類型:ToIntBiFunction <T,U>寞酿,ToLongBiFunction <T家夺,U>和 ToDoubleBiFunction <T,U>伐弹。Consumer 的兩個(gè)參數(shù)變體采用一個(gè)對(duì)象引用和一個(gè)基本類型:ObjDoubleConsumer <T>拉馋,ObjIntConsumer <T>和 ObjLongConsumer <T>〔液茫總共有九個(gè)基本接口的雙參數(shù)版本煌茴。

??最后,還有 BooleanSupplier 接口昧狮,這是 Supplier 的一個(gè)變量景馁,它返回布爾值。這是任何標(biāo)準(zhǔn)功能接口名稱中唯一明確提到的布爾類型逗鸣,但是通過(guò) Predicate 及其四種變體形式支持返回布爾值合住。BooleanSupplier 接口和前面段落中描述的四十二個(gè)接口占所有四十三個(gè)標(biāo)準(zhǔn)功能接口绰精。不可否認(rèn),這是一個(gè)很大的合并透葛,而不是非常正交(Admittedly, this is a lot to swallow, and not terribly orthogonal)笨使。另一方面,你需要的大部分功能接口都是為你編寫的僚害,并且它們的名稱足夠常規(guī)硫椰,以便你在需要時(shí)不會(huì)遇到太多麻煩。

??大多數(shù)標(biāo)準(zhǔn)功能接口僅提供對(duì)基本類型的支持萨蚕。不要試圖用基本類型的包裝類來(lái)使用基本的功能性接口靶草,而不是用基本類型的功能性接口(Don’t be tempted to use basic functional interfaces with boxed primitives instead of primitive functional interfaces)。 雖然它有效岳遥,但是它違了第 61 項(xiàng)的建議奕翔,“基本類型優(yōu)先于裝箱基本類型”。使用裝箱基本類型進(jìn)行批量操作可能會(huì)導(dǎo)致致命的性能后果浩蓉。

??現(xiàn)在你知道派继,通常【情況下】應(yīng)該使用標(biāo)準(zhǔn)功能性接口而不是編寫自己的接口捻艳。但你應(yīng)該什么時(shí)候?qū)懽约旱摹竟δ苄越涌谀亍考菘撸慨?dāng)然,如果那些標(biāo)準(zhǔn)【接口】沒(méi)有符合您的需要认轨,您需要自己編寫绅络,例如,如果您需要一個(gè)帶有三個(gè)參數(shù)的謂詞(predicate)好渠,或者一個(gè)拋出已檢查異常的謂詞(predicate)昨稼。但有時(shí)你應(yīng)該編寫自己的功能性接口,即使其中一個(gè)標(biāo)準(zhǔn)結(jié)構(gòu)完全相同拳锚。

??考慮我們的老朋友 Comparator<T>假栓,它在結(jié)構(gòu)上與 ToIntBiFunction <T,T>接口相同霍掺。即使后者接口已經(jīng)存在匾荆,當(dāng)前者被添加到庫(kù)中時(shí),使用它也是錯(cuò)誤的杆烁。Comparator 有幾個(gè)值得擁有自己的接口原因牙丽。首先,它的名稱每次在 API 中使用時(shí)都提供了優(yōu)秀的文檔兔魂,并且它被大量使用烤芦。其次,Comparator 接口對(duì)構(gòu)成有效實(shí)例的內(nèi)容有很強(qiáng)的要求析校,有效實(shí)例包含其通用約定( general contract)构罗。通過(guò)接口的實(shí)現(xiàn)铜涉,你承諾遵守其約定。第三遂唧,接口配備了大量有用的默認(rèn)方法來(lái)轉(zhuǎn)換和組合比較器(comparators)芙代。

??如果你需要一個(gè)與 Comparator 共享以下一個(gè)或多個(gè)特性的功能接口,您應(yīng)該認(rèn)真考慮編寫專用的功能接口而不是使用標(biāo)準(zhǔn)接口:

  • 它將被普遍使用盖彭,并可從描述性名稱中受益纹烹。
  • 它與之相關(guān)的約定很強(qiáng)(It has a strong contract associated with it)。
  • 它將受益于自定義的默認(rèn)方法召边。

??如果您選擇編寫自己的功能性接口铺呵,請(qǐng)記住它是一個(gè)界面,因此應(yīng)該非常謹(jǐn)慎地設(shè)計(jì)(第 21 項(xiàng))掌实。

??請(qǐng)注意陪蜻,EldestEntryRemovalFunction 接口(原書第 199 頁(yè))標(biāo)有@FunctionalInterface 注釋。此注釋類型在靈魂(spirit)上與@Override 類似贱鼻。它是程序員意圖的聲明,有三個(gè)目的:它告訴讀者該類及其文檔滋将,該接口旨在啟用 lambda邻悬;它保持誠(chéng)實(shí),因?yàn)槿绻鄠€(gè)抽象方法随闽,接口就無(wú)法編譯父丰;并且它可以防止維護(hù)者在接口升級(jí)時(shí)意外地將抽象方法添加到接口。始終使用@FunctionalInterface 注釋來(lái)注釋您的功能接口掘宪。

??最后應(yīng)該關(guān)心的點(diǎn)是關(guān)于 API 中功能性接口的使用蛾扇。如果在客戶端中有可能產(chǎn)生歧義,則不要提供具有多個(gè)重載的方法魏滚,這些方法在相同的參數(shù)位置采用不同的功能接口镀首。這不僅僅是一個(gè)理論問(wèn)題。ExecutorService 的 submit 方法可以采用 Callable <T>或 Runnable鼠次,并且可以編寫一個(gè)需要強(qiáng)制轉(zhuǎn)換的客戶端程序來(lái)表示正確的重載(第 52 項(xiàng))更哄。避免此問(wèn)題的最簡(jiǎn)單方法是不要編寫在同一參數(shù)位置使用不同功能接口的重載。這是第 52 項(xiàng)建議中的一個(gè)特例腥寇,“慎用重載”成翩。

??總而言之,既然 Java 已經(jīng)有了 lambda赦役,那么在設(shè)計(jì) API 時(shí)必須考慮到 lambda麻敌。接受輸入上的功能接口類型并在輸出上返回它們。通常最好使用 java.util.function.Function 中提供的標(biāo)準(zhǔn)接口掂摔,但請(qǐng)注意那些相對(duì)少見(jiàn)的情況术羔,那就最好編寫自己的功能接口职辅。

45.謹(jǐn)慎使用 Stream

??在 Java 8 中添加了 Stream API,以簡(jiǎn)化串行或并行批量執(zhí)行操作的任務(wù)聂示。這個(gè) API 提供了兩個(gè)關(guān)鍵的抽象概念:流(stream)表示有限或無(wú)限的數(shù)據(jù)元素序列域携,流管道(stream pipeline)表示對(duì)這些元素的多級(jí)計(jì)算。流中的元素可以來(lái)自任何地方鱼喉。常見(jiàn)的來(lái)源包括集合秀鞭,數(shù)組,文件扛禽,正則表達(dá)式模式匹配器锋边,偽隨機(jī)數(shù)生成器和其他流。流中的數(shù)據(jù)元素可以是對(duì)象的引用或基本類型编曼。支持三種基本類型:int豆巨,long 和 double。

??流管道由源流和零個(gè)或多個(gè)中間操作(intermediate operations )以及一個(gè)終端操作( terminal operation)組成掐场。每個(gè)中間操作以某種方式轉(zhuǎn)換流往扔,例如將每個(gè)元素映射到該元素的函數(shù)或過(guò)濾掉不滿足某些條件的所有元素。中間操作都將一個(gè)流轉(zhuǎn)換為另一個(gè)流熊户,其元素類型可以與輸入流相同或與之不同萍膛。終端操作對(duì)從最后的中間操作產(chǎn)生的流執(zhí)行最終計(jì)算,例如將其元素存儲(chǔ)到集合中嚷堡,返回某個(gè)元素或打印其所有元素蝗罗。

??流管道是懶求值(evaluated lazily):在調(diào)用終端操作之前是不會(huì)開始求值的,并且不會(huì)去計(jì)算那些在完成終端操作的過(guò)程中不需要的數(shù)據(jù)元素蝌戒。這種懶求值使得可以使用無(wú)限流串塑。請(qǐng)注意,沒(méi)有終端操作的流管道是靜默無(wú)操作的北苟,因此不要忘記包含一個(gè)【終端操作】(Stream pipelines are evaluated lazily: evaluation doesn’t start until the terminal operation is invoked, and data elements that aren’t required in order to complete the terminal operation are never computed. This lazy evaluation is what makes it possible to work with infinite streams. Note that a stream pipeline without a terminal operation is a silent no-op, so don’t forget to include one. )桩匪。

??流 API 非常流暢:它旨在允許將構(gòu)成管道的所有調(diào)用鏈接(chain)到單個(gè)表達(dá)式中。實(shí)際上粹淋,多個(gè)管道可以鏈接(chain)在一起形成一個(gè)表達(dá)式吸祟。

??默認(rèn)情況下,流管道按順序運(yùn)行桃移。使管道并行執(zhí)行就像在管道中的任何流上調(diào)用并行方法一樣簡(jiǎn)單屋匕,但很少這樣做(第 48 項(xiàng))。

??流 API 具有足夠的通用性(The streams API is sufficiently versatile)借杰,幾乎任何計(jì)算都可以使用流來(lái)執(zhí)行过吻,但僅僅因?yàn)槟憧梢赃@么做并不意味著你應(yīng)該這樣做。如果使用得當(dāng),流可以使程序更短更清晰; 如果使用不當(dāng)纤虽,可能會(huì)使程序難以閱讀和維護(hù)乳绕。

??考慮以下程序,該程序從字典文件中讀取單詞并打印其大小符合用戶指定的最小值的所有相同字母異序詞組(anagram groups)逼纸⊙蟠耄回想一下,如果兩個(gè)單詞由不同順序的相同字母組成杰刽,則它們是相同字母異序詞菠发。程序從用戶指定的字典文件中讀取每個(gè)單詞并將單詞放入 map 中。map 的鍵是用字母按字母順序排列的單詞贺嫂,因此“staple”的鍵是“aelpst”滓鸠,“petals”的鍵也是“aelpst”:兩個(gè)單詞是相同字母異序詞,所有的相同字母異序詞共享相同的字母形式(或 alphagram第喳,因?yàn)樗袝r(shí)是已知的((or alphagram, as it is sometimes known))糜俗。map 的值是包含按字母順序排列的共享形式的所有單詞的列表。字典處理完畢后曲饱,每個(gè)列表都是一個(gè)完整的相同字母異序詞組悠抹。然后程序遍歷 map 的 values()并打印每個(gè)大小符合閾值的列表:

// Prints all large anagram groups in a dictionary iteratively
public class Anagrams {
    public static void main(String[] args) throws IOException {
        File dictionary = new File(args[0]);
        int minGroupSize = Integer.parseInt(args[1]);
        Map<String, Set<String>> groups = new HashMap<>();
        try (Scanner s = new Scanner(dictionary)) {
            while (s.hasNext()) {
                String word = s.next();
                groups.computeIfAbsent(alphabetize(word), (unused) -> new TreeSet<>()).add(word);
            }
        }
        for (Set<String> group : groups.values())
            if (group.size() >= minGroupSize)
                System.out.println(group.size() + ": " + group);
    }

    private static String alphabetize(String s) {
        char[] a = s.toCharArray();
        Arrays.sort(a);
        return new String(a);
    }
}

??該計(jì)劃的一個(gè)步驟值得注意。將每個(gè)單詞插入到 map 中(以粗體顯示的:groups.computeIfAbsent(alphabetize(word), (unused) -> new TreeSet<>()).add(word);)使用了在 Java 8 中添加的 computeIfAbsent 方法渔工。此方法在 map 中查找鍵:如果鍵存在锌钮,則該方法僅返回與其關(guān)聯(lián)的值。如果不是引矩,則該方法通過(guò)將給定的函數(shù)對(duì)象應(yīng)用于鍵來(lái)計(jì)算值,將該值與鍵相關(guān)聯(lián)侵浸,并返回計(jì)算的值旺韭。computeIfAbsent 方法簡(jiǎn)化了將多個(gè)值與每個(gè)鍵相關(guān)聯(lián)的映射的實(shí)現(xiàn)。

??現(xiàn)在考慮以下程序掏觉,它解決了同樣的問(wèn)題区端,但大量使用了流。請(qǐng)注意澳腹,除了打開字典文件的代碼之外织盼,整個(gè)程序都包含在一個(gè)表達(dá)式中。在單獨(dú)的表達(dá)式中打開字典的唯一原因是允許使用 try-with-resources 語(yǔ)句酱塔,以確保字典文件已關(guān)閉:

// Overuse of streams - don't do this!
public class Anagrams {
    public static void main(String[] args) throws IOException {
        Path dictionary = Paths.get(args[0]);
        int minGroupSize = Integer.parseInt(args[1]);
        try (Stream<String> words = Files.lines(dictionary)) {
            words.collect(
                groupingBy(word -> word.chars().sorted()
                    .collect(StringBuilder::new,
                    (sb, c) -> sb.append((char) c),
                    StringBuilder::append).toString()))
            .values().stream()
            .filter(group -> group.size() >= minGroupSize)
            .map(group -> group.size() + ": " + group)
            .forEach(System.out::println);
        }
    }
}

??如果你發(fā)現(xiàn)此代碼難以閱讀沥邻,請(qǐng)不要擔(dān)心; 你不是一個(gè)人。它更短羊娃,但可讀性更小唐全,特別是對(duì)于不是使用流的專家級(jí)程序猿。過(guò)度使用流會(huì)使程序難以閱讀和維護(hù)蕊玷。

??幸運(yùn)的是邮利,有一個(gè)讓人開心的工具弥雹。以下程序使用流而不會(huì)過(guò)度使用流來(lái)解決相同的問(wèn)題。結(jié)果是一個(gè)比原始程序更短更清晰的程序:

// Tasteful use of streams enhances clarity and conciseness
public class Anagrams {
    public static void main(String[] args) throws IOException {
        Path dictionary = Paths.get(args[0]);
        int minGroupSize = Integer.parseInt(args[1]);
        try (Stream<String> words = Files.lines(dictionary)) {
            words.collect(groupingBy(word -> alphabetize(word)))
                .values().stream()
                .filter(group -> group.size() >= minGroupSize)
                .forEach(g -> System.out.println(g.size() + ": " + g));
        }
    }
    // alphabetize method is the same as in original version
}

??即使你以前很少接觸過(guò)流延届,這個(gè)程序也不難理解剪勿。它在 try-with-resources 塊中打開字典文件,獲取包含文件中所有行的流方庭。stream 變量被命名為 words厕吉,表示流中的每個(gè)元素都是一個(gè) word。此流上的管道沒(méi)有中間操作; 它的終端操作將所有 word 收集到一個(gè) map 中二鳄,該 map 按字母順序排列單詞(第 46 項(xiàng))赴涵。這與在以前版本的程序中構(gòu)建的 map 完全相同。然后在 map 的 values()中打開一個(gè)新的 Stream<List<String>>订讼。當(dāng)然髓窜,這個(gè)流中的元素是相同字母異序詞組。過(guò)濾流以便忽略大小小于 minGroupSize 的所有組欺殿,最后寄纵,通過(guò)終端操作 forEach 打印剩余的組。

??請(qǐng)注意脖苏,小心選擇了 lambda 參數(shù)名稱程拭。參數(shù) g 應(yīng)該真正命名為 group,但是生成的代碼行對(duì)于本書來(lái)說(shuō)太寬了棍潘。在沒(méi)有顯式類型的情況下恃鞋,仔細(xì)命名 lambda 參數(shù)對(duì)于流管道的可讀性至關(guān)重要

??另外請(qǐng)注意亦歉,單詞字母化是在單獨(dú)的 alphabetize 方法中完成的恤浪。這通過(guò)提供操作的名稱并將實(shí)現(xiàn)細(xì)節(jié)保留在主程序之外來(lái)增強(qiáng)可讀性。使用輔助方法對(duì)于流管道中的可讀性比在迭代代碼中更為重要肴楷,因?yàn)楣艿廊鄙亠@式類型信息和命名臨時(shí)變量水由。

??可以使用流重新實(shí)現(xiàn) alphabetize 方法,但是基于流的 alphabetize 方法不太清晰赛蔫,更難以正確編寫砂客,并且可能更慢。這些缺陷是由于 Java 缺乏對(duì)原始 char 流的支持(這并不意味著 Java 應(yīng)該支持 char 流呵恢;這樣做是不可行的)鞠值。要演示使用流處理 char 值的危險(xiǎn),請(qǐng)考慮以下代碼:

"Hello world!".chars().forEach(System.out::print);

??你可能希望它打印 Hello world瑰剃!齿诉,但如果你運(yùn)行它,你會(huì)發(fā)現(xiàn)它打印 721011081081113211911111410810033。這是因?yàn)椤癏ello world粤剧!”.chars()返回的流的元素不是 char 值而是 int 值歇竟,因此調(diào)用的是 print 的 int 重載【方法】。令人遺憾的是抵恋,名為 chars 的方法返回一個(gè) int 值流焕议。你可以通過(guò)使用強(qiáng)制轉(zhuǎn)換來(lái)強(qiáng)制調(diào)用正確的重載來(lái)修復(fù)程序:

"Hello world!".chars().forEach(x -> System.out.print((char) x));

??但理想情況下,你應(yīng)該避免使用流來(lái)處理 char 值弧关。

??當(dāng)你開始使用流時(shí)盅安,你可能會(huì)有將所有循環(huán)轉(zhuǎn)換為流的沖動(dòng)的感覺(jué),但要抵制這種沖動(dòng)世囊。盡管這只是有可能發(fā)生别瞭,但它會(huì)損害代碼庫(kù)的可讀性和可維護(hù)性。通常株憾,使用流和遍歷的某種組合可以最好地完成中等復(fù)雜程度的任務(wù)蝙寨,如上面的 Anagrams 程序所示。因此嗤瞎,重構(gòu)現(xiàn)有代碼以使用流墙歪,并僅在有意義的情況下在新代碼中使用它們

??如該項(xiàng)目中的程序所示贝奇,流管道使用函數(shù)對(duì)象(通常是 lambdas 或方法引用)表示重復(fù)計(jì)算虹菲,而遍歷代碼使用代碼塊表示重復(fù)計(jì)算。以下操作你可以在代碼塊中執(zhí)行掉瞳,但無(wú)法在函數(shù)對(duì)象中執(zhí)行:

  • 在代碼塊中毕源,你可以讀取或修改范圍內(nèi)的任何局部變量; 在 lambda 中,你只能讀取最終或有效的最終變量[JLS 4.12.4],并且你無(wú)法修改任何局部變量。

  • 在代碼塊中僵腺,不可以從封閉方法返回呵晚,中斷或繼續(xù)封閉循環(huán),或拋出聲明此方法被拋出的任何已受檢異常; 在一個(gè) lambda 你無(wú)法做到這些事情必盖。

??如果使用這些技巧可以更好地表達(dá)計(jì)算【過(guò)程】拌牲,那么流就可能不是最好的方式(If a computation is best expressed using these techniques, then it’s probably not a good match for streams)。相反歌粥,流可以很容易做一些事情:

  • 均勻地轉(zhuǎn)換元素序列
  • 過(guò)濾元素序列
  • 使用單個(gè)操作組合元素序列(例如塌忽,添加它們,串聯(lián)(concatenate )它們或計(jì)算它們的最小值)
  • 將元素序列累積(accumulate)到集合中失驶,或者通過(guò)一些常見(jiàn)屬性對(duì)它們進(jìn)行分組
  • 在元素序列中搜索滿足某個(gè)條件的元素

??如果使用這些技巧可以更好地表達(dá)計(jì)算【過(guò)程】土居,那么流是它的良好候選者。

??使用流很難做的一件事是同時(shí)從管道的多個(gè)階段訪問(wèn)相應(yīng)的元素:一旦將值映射到某個(gè)其他值,原始值就會(huì)丟失擦耀。一種解決方法是將每個(gè)值映射到包含原始值和新值的對(duì)對(duì)象(pair object)棉圈,但這不是一個(gè)令人滿意的解決方案,尤其是如果管道的多個(gè)階段需要對(duì)對(duì)象眷蜓。由此產(chǎn)生的代碼是混亂和冗長(zhǎng)的分瘾,這破壞了流的主要目的。如果適當(dāng)使用的話吁系,更好的解決方法是在需要訪問(wèn)早期階段值的時(shí)候反轉(zhuǎn)映射德召。(When it is applicable, a better workaround is to invert the mapping when you need access to the earlier-stage value)。

??例如汽纤,讓我們編寫一個(gè)程序來(lái)打印前 20 個(gè)梅森素?cái)?shù)(Mersenne primes)上岗。為了更新你的記憶,梅森數(shù)是一個(gè) 2^p-1 的數(shù)字蕴坪。如果 p 是素?cái)?shù)肴掷,相應(yīng)的梅森數(shù)可能是素?cái)?shù); 如果是這樣的話,那就是梅森素?cái)?shù)辞嗡。作為我們管道中的初始流捆等,我們需要所有素?cái)?shù)。這是一種返回該(無(wú)限)流的方法续室。我們假設(shè)使用靜態(tài)導(dǎo)入來(lái)輕松訪問(wèn) BigInteger 的靜態(tài)成員:

static Stream<BigInteger> primes() {
    return Stream.iterate(TWO, BigInteger::nextProbablePrime);
}

??方法(primes)的名稱是描述流的元素的復(fù)數(shù)名詞栋烤。強(qiáng)烈建議所有返回流的方法使用此命名約定,因?yàn)樗鰪?qiáng)了流管道的可讀性挺狰。該方法使用靜態(tài)工廠 Stream.iterate明郭,它接受兩個(gè)參數(shù):流中的第一個(gè)元素,以及從前一個(gè)元素生成流中的下一個(gè)元素的函數(shù)丰泊。這是打印前 20 個(gè)梅森素?cái)?shù)的程序:

public static void main(String[] args) {
    primes().map(p -> TWO.pow(p.intValueExact()).subtract(ONE))
        .filter(mersenne -> mersenne.isProbablePrime(50))
        .limit(20)
        .forEach(System.out::println);
}

??這個(gè)程序是上文描述中的直接編碼:它從素?cái)?shù)開始薯定,計(jì)算相應(yīng)的梅森數(shù),過(guò)濾掉除素?cái)?shù)之外的所有數(shù)字(幻數(shù) 50 控制概率素性測(cè)試(the magic number 50 controls the probabilistic primality tes))瞳购,將得到的流限制為 20 個(gè)元素话侄,并打印出來(lái)。

??現(xiàn)在假設(shè)我們想要在每個(gè)梅森素?cái)?shù)之前加上它的指數(shù)(p)学赛。該值僅出現(xiàn)在初始流中年堆,因此在終端操作中無(wú)法訪問(wèn),從而打印結(jié)果盏浇。幸運(yùn)的是变丧,通過(guò)反轉(zhuǎn)第一個(gè)中間操作中發(fā)生的映射,可以很容易地計(jì)算出梅森數(shù)的指數(shù)绢掰。指數(shù)只是二進(jìn)制表示中的位數(shù)痒蓬,因此該終端操作生成所需的結(jié)果:

.forEach(mp -> System.out.println(mp.bitLength() + ": " + mp));

??有很多任務(wù)童擎,無(wú)論是使用流還是迭代都不明顯。例如攻晒,考慮初始化一副新牌的任務(wù)顾复。假設(shè) Card 是一個(gè)值不可變的類,它封裝了 Rank 和 Suit炎辨,兩者都是枚舉類型捕透。此任務(wù)代表任何需要的計(jì)算可以從兩組中選擇所有元素對(duì)的任務(wù)。數(shù)學(xué)家稱之為兩組的笛卡爾積(Cartesian product )碴萧。這是一個(gè)帶有嵌套 for-each 循環(huán)的迭代實(shí)現(xiàn)乙嘀,對(duì)你來(lái)說(shuō)應(yīng)該很熟悉:

// Iterative Cartesian product computation
private static List<Card> newDeck() {
    List<Card> result = new ArrayList<>();
    for (Suit suit : Suit.values())
        for (Rank rank : Rank.values())
            result.add(new Card(suit, rank));
    return result;
}

??這是一個(gè)基于流的實(shí)現(xiàn),它使用了中間操作 flatMap破喻。此操作將流中的每個(gè)元素映射到流虎谢,然后將所有這些新流連接成單個(gè)流(或展平它們(or flattens them))。請(qǐng)注意曹质,此實(shí)現(xiàn)包含嵌套的 lambda婴噩,以粗體顯示;

// Stream-based Cartesian product computation
private static List<Card> newDeck() {
    return Stream.of(Suit.values())
        .flatMap(suit ->
            Stream.of(Rank.values())
                .map(rank -> new Card(suit, rank)))
        .collect(toList());
}

??newDeck 的兩個(gè)版本中哪一個(gè)更好羽德?它歸結(jié)為個(gè)人偏好和你的編程環(huán)境几莽。第一個(gè)版本更簡(jiǎn)單,也許感覺(jué)更自然宅静。大部分 Java 程序猿將能夠理解和維護(hù)它章蚣,但是一些程序猿會(huì)對(duì)第二個(gè)(基于流的)版本感覺(jué)更舒服。如果你對(duì)流和函數(shù)式編程很精通姨夹,那么它會(huì)更簡(jiǎn)潔纤垂,也不會(huì)太難理解。如果你不確定自己喜歡哪個(gè)版本磷账,則迭代版本可能是更安全的選擇峭沦。如果你更喜歡流版本,并且你相信其他使用該代碼的程序猿跟你有共同的偏好逃糟,那么你應(yīng)該使用它吼鱼。

??總之,一些任務(wù)最好用流完成绰咽,其他任務(wù)最好用遍歷完成蛉抓。通過(guò)組合這兩種方法可以最好地完成許多任務(wù)。選擇哪種方法用于任務(wù)沒(méi)有硬性規(guī)定剃诅,但有一些有用的啟發(fā)式方法。在許多情況下驶忌,將清楚使用哪種方法; 在某些情況下矛辕,它不會(huì)笑跛。如果你不確定某個(gè)任務(wù)是否更適合流或遍歷,那么就兩個(gè)都嘗試一下聊品,并看一下哪個(gè)更好飞蹂。

46.優(yōu)先選擇 Stream 中無(wú)副作用的函數(shù)

??如果你是一個(gè)【使用】流的新手,可能很難掌握它們翻屈。僅僅將你的計(jì)算【過(guò)程】表示為流管道可能很難陈哑。當(dāng)你成功的時(shí)候【成功地將計(jì)算過(guò)程用流管道表示出來(lái)】,你的程序會(huì)運(yùn)行伸眶,但你可能幾乎沒(méi)有任何好處惊窖。Streams 不僅僅是一個(gè) API,它還是一個(gè)基于函數(shù)式編程的范例厘贼。為了獲得流必須提供的表現(xiàn)力界酒,速度和某些情況下的并行性,你必須采用范例和 API嘴秸。

??流范例中最重要的部分是將計(jì)算結(jié)構(gòu)化為一系列轉(zhuǎn)換毁欣,其中每個(gè)階段的結(jié)果盡可能接近前一階段結(jié)果的純函數(shù)( pure function )。純函數(shù)的【執(zhí)行】結(jié)果取決于其輸入:它不依賴于任何可變狀態(tài)岳掐,也不更新任何狀態(tài)凭疮。為了實(shí)現(xiàn)這一點(diǎn),你傳遞給流操作的任何函數(shù)對(duì)象(中間或終端)都應(yīng)該沒(méi)有副作用串述。

??有時(shí)执解,你可能會(huì)看到類似于此代碼段的流代碼,它會(huì)在文本文件中構(gòu)建單詞的頻率表:

// Uses the streams API but not the paradigm--Don't do this!
Map<String, Long> freq = new HashMap<>();
try (Stream<String> words = new Scanner(file).tokens()) {
    words.forEach(word -> {
        freq.merge(word.toLowerCase(), 1L, Long::sum);
    });
}

??這段代碼出了什么問(wèn)題剖煌?畢竟材鹦,它使用流,lambdas 和方法引用耕姊,并得到正確的答案桶唐。簡(jiǎn)單地說(shuō),它根本不是流代碼; 它的迭代代碼偽裝成流代碼茉兰。它沒(méi)有從流 API 中獲益尤泽,并且它比相應(yīng)的迭代代碼更長(zhǎng),更難以閱讀规脸,并且可維護(hù)性更小坯约。問(wèn)題源于這樣一個(gè)事實(shí):這個(gè)代碼在一個(gè)終端 forEach 操作中完成所有工作,使用一個(gè)變異外部狀態(tài)的 lambda(頻率表)莫鸭。執(zhí)行除了呈現(xiàn)流執(zhí)行的計(jì)算結(jié)果之外的任何操作的 forEach 操作都是“代碼中的壞味道”闹丐,就比如一個(gè)變異狀態(tài)的 lambda。那么這段代碼應(yīng)該怎么樣被因?

// Proper use of streams to initialize a frequency table
Map<String, Long> freq;
try (Stream<String> words = new Scanner(file).tokens()) {
    freq = words
        .collect(groupingBy(String::toLowerCase, counting()));
}

??此代碼段與前一代碼相同卿拴,但正確使用了流 API衫仑。它更短更清晰。那么為什么有人會(huì)用另一種方式寫呢堕花? 因?yàn)樗褂昧怂麄円呀?jīng)熟悉的工具文狱。Java 程序員知道如何使用 for-each 循環(huán),而 forEach 終端操作是類似的缘挽。但 forEach 操作是終端操作中最不強(qiáng)大的操作之一瞄崇,也是最不友好的流操作。它很顯然是使用了迭代壕曼,因此不適合并行化苏研。forEach 操作應(yīng)僅用于報(bào)告流計(jì)算的結(jié)果,而不是用于執(zhí)行計(jì)算窝稿。有時(shí)楣富,將 forEach 用于其他目的是有意義的,例如將流計(jì)算的結(jié)果添加到預(yù)先存在的集合中伴榔。

??改進(jìn)的代碼使用了一個(gè)收集器(collector)纹蝴,這是一個(gè)新概念,你必須學(xué)習(xí)了才能使用流踪少。Collectors API 是令人生畏的:它有三十九種方法塘安,其中一些方法有多達(dá)五種類型參數(shù)。好消息是援奢,你可以從這個(gè) API 中獲得大部分好處兼犯,而無(wú)需深入研究其完整的復(fù)雜性。對(duì)于初學(xué)者集漾,你可以忽略 Collector 接口切黔,并將收集器視為封裝縮減策略的不透明對(duì)象(an opaque object that encapsulates a reduction strategy)。在這種情況下具篇,縮減意味著將流的元素組合成單個(gè)對(duì)象纬霞。收集器生成的對(duì)象通常是一個(gè)集合(它代表名稱收集器((which accounts for the name collector))。

??用于將流的元素收集到真正的集合中的收集器是很簡(jiǎn)單的驱显。有三個(gè)這樣的收集器:toList()诗芜,toSet()和 toCollection(collectionFactory)。它們分別返回一個(gè)集合埃疫,一個(gè)列表和一個(gè)程序猿指定的集合類型伏恐。有了這些知識(shí),我們可以編寫一個(gè)流管道來(lái)從頻率表中提取前十個(gè)列表栓霜。

// Pipeline to get a top-ten list of words from a frequency table
List<String> topTen = freq.keySet().stream()
    .sorted(comparing(freq::get).reversed())
    .limit(10)
    .collect(toList());

??請(qǐng)注意翠桦,我們沒(méi)有使用其類 Collectors 限定 toList 方法。習(xí)慣性地將收集器的所有成員都靜態(tài)導(dǎo)入是明智的胳蛮,因?yàn)樗沽鞴艿栏呖勺x性秤掌。

??這段代碼中唯一棘手的是我們傳遞給 sorted【方法】的部分愁铺,compare(freq::get).reversed()的比較器。comparing 方法是采用密鑰提取功能的比較器構(gòu)造方法(第 14 項(xiàng))闻鉴。該函數(shù)接收一個(gè)單詞,“提让鳌(extraction)”實(shí)際上是一個(gè)表查找:綁定方法引用 freq::get 在頻率表中查找單詞并返回單詞在文件中出現(xiàn)的次數(shù)孟岛。最后,我們?cè)诒容^器上調(diào)用 reverse督勺,因此我們將單詞【出現(xiàn)的頻率】從最頻繁到最不頻繁進(jìn)行排序渠羞。然后將流限制為十個(gè)單詞并將它們收集到一個(gè)列表中是一件簡(jiǎn)單的事情。

??之前的代碼片段使用 Scanner 的流方法通過(guò)掃描程序獲取流智哀。該方法時(shí)在 Java 9 中添加的次询。如果你使用的是早期版本,則可以使用類似于第 47 項(xiàng)(streamOf(Iterable <E>))的適配器來(lái)將實(shí)現(xiàn)了 Iterator 的 scanner 轉(zhuǎn)換為流瓷叫。

??那么 Collectors 的其他 36 種方法呢屯吊?它們中的大多數(shù)存在是為了讓你將流收集到 map 中,這比將它們收集到真實(shí)集合中要復(fù)雜得多摹菠。每個(gè)流元素與鍵和值相關(guān)聯(lián)盒卸,并且多個(gè)流元素可以與相同的鍵相關(guān)聯(lián)。

??最簡(jiǎn)單的 map 收集器是 toMap(keyMapper次氨,valueMapper)蔽介,它接受兩個(gè)函數(shù),其中一個(gè)函數(shù)將一個(gè)流元素映射到一個(gè)鍵煮寡,另一個(gè)函數(shù)映射到一個(gè)值虹蓄。我們?cè)诘?34 項(xiàng)的 fromString 實(shí)現(xiàn)中使用了這個(gè)收集器來(lái)創(chuàng)建從枚舉的字符串形式到枚舉本身的映射:

// Using a toMap collector to make a map from string to enum
private static final Map<String, Operation> stringToEnum = Stream.of(values()).collect(toMap(Object::toString, e -> e));

??如果流中的每個(gè)元素都映射到唯一鍵,則這種簡(jiǎn)單的 toMap 形式是完美的幸撕。 如果多個(gè)流元素映射到同一個(gè)鍵薇组,則管道將以 IllegalStateException 異常來(lái)終止【計(jì)算】。

??更復(fù)雜的 toMap 形式(比如 groupingBy 方法)為你提供了各種方法來(lái)提供處理此類沖突的策略杈帐。一種方法是除了鍵和值映射器之外体箕,還為 toMap 方法提供合并函數(shù)。合并函數(shù)是 BinaryOperator<V>挑童,其中 V 是映射的值類型累铅。使用合并函數(shù)將與鍵關(guān)聯(lián)的任何其他值與現(xiàn)有值組合,因此站叼,例如娃兽,如果合并函數(shù)是乘法,則通過(guò)值映射最終得到的值是與鍵關(guān)聯(lián)的所有值的乘積尽楔。

??toMap 的三參數(shù)形式對(duì)于創(chuàng)建從鍵到與該鍵關(guān)聯(lián)的所選元素的映射也很有用投储。例如第练,假設(shè)我們有各種藝術(shù)家的唱片專輯流,我們想要一個(gè)從錄音藝術(shù)家到最暢銷專輯的 map 映射玛荞。這個(gè) collector 就能完成這項(xiàng)工作娇掏。

// Collector to generate a map from key to chosen element for key
Map<Artist, Album> topHits = albums.collect(toMap(Album::artist, a->a, maxBy(comparing(Album::sales))));

??請(qǐng)注意,比較器使用靜態(tài)工廠方法 maxBy勋眯,它是從 BinaryOperator 靜態(tài)導(dǎo)入的婴梧。此方法將 Comparator<T>轉(zhuǎn)換為 BinaryOperator<T>,用于計(jì)算指定比較器隱含的最大值客蹋。在這種情況下塞蹭,比較器由比較器構(gòu)造方法 comparing 返回,它采用密鑰提取器功能(key extractor function)Album::sales讶坯。這可能看起來(lái)有點(diǎn)復(fù)雜番电,但代碼可讀性很好。簡(jiǎn)而言之辆琅,它說(shuō)漱办,“將專輯流轉(zhuǎn)換為 map,將每位藝術(shù)家映射到銷售量最佳專輯的專輯涎跨⊥荻常”這接近問(wèn)題的陳述【程度】令人感到驚訝【意思就是說(shuō)這代碼的意思很接近問(wèn)題的描述(OS:臭不要臉)】。

??toMap 的三參數(shù)形式的另一個(gè)用途是產(chǎn)生一個(gè)收集器隅很,當(dāng)發(fā)生沖突時(shí)強(qiáng)制執(zhí)行 last-write-wins 策略【保留最后一個(gè)沖突值】撞牢。對(duì)于許多流,結(jié)果將是不確定的叔营,但如果映射函數(shù)可能與鍵關(guān)聯(lián)的所有值都相同屋彪,或者它們都是可接受的,則此收集器的行為可能正是你想要的:

// Collector to impose last-write-wins policy
toMap(keyMapper, valueMapper, (v1, v2) -> v2)

??toMap 的第三個(gè)也是最后一個(gè)版本采用第四個(gè)參數(shù)绒尊,即一個(gè) map 工廠畜挥,用于指定特定的 map 實(shí)現(xiàn),例如 EnumMap 或 TreeMap婴谱。

??toMap 的前三個(gè)版本也有變體形式蟹但,名為 toConcurrentMap,它們并行高效運(yùn)行并生成 ConcurrentHashMap 實(shí)例谭羔。

??除了 toMap 方法之外华糖,Collectors API 還提供了 groupingBy 方法,該方法返回【一個(gè)】收集器用來(lái)生成基于分類器函數(shù)(classifier function)將元素分組到類別中的映射瘟裸。分類器函數(shù)接收一個(gè)元素并返回它的所屬類別客叉。此類別用作元素的 map 的鍵。groupingBy 方法的最簡(jiǎn)單版本是僅采用分類器并返回一個(gè)映射,其值是每個(gè)類別中所有元素的列表兼搏。這是我們?cè)诘?45 項(xiàng)中的 Anagram 程序中使用的收集器卵慰,用于生成從按字母順序排列的單詞到共享字母順序的單詞列表的映射:

words.collect(groupingBy(word -> alphabetize(word)))

??如果希望 groupingBy 返回一個(gè)生成帶有除列表之外的值的映射的收集器,則除了分類器之外佛呻,還可以指定下游收集器(downstream collector)裳朋。下游收集器從一個(gè)包含類別中所有元素的流中生成一個(gè)值。此參數(shù)的最簡(jiǎn)單用法是傳遞 toSet()吓著,這將生成一個(gè)映射再扭,其值是元素集而不是列表。這會(huì)生成一個(gè)映射夜矗,該映射將每個(gè)類別與類別中的元素?cái)?shù)相關(guān)聯(lián),而不是包含元素的集合让虐。這就是你在本項(xiàng)目開頭的頻率表示例中看到的內(nèi)容:

Map<String, Long> freq = words.collect(groupingBy(String::toLowerCase, counting()));

??groupingBy 的第三個(gè)版本允許你指定除下游收集器之外的 map 工廠紊撕。請(qǐng)注意,此方法違反了標(biāo)準(zhǔn)的 telescoping 參數(shù)列表模式:mapFactory 參數(shù)位于 downStream 參數(shù)之前赡突,而不是之后对扶。此版本的 groupingBy 使你可以控制包含的映射以及包含的集合(This version of groupingBy gives you control over the containing map as well as the contained collections),因此惭缰,例如浪南,你可以指定一個(gè)收集器,該收集器返回一個(gè) value 為 TreeSet 的 TreeMap漱受。

??groupingByConcurrent 方法提供了 groupingBy 的所有三個(gè)重載的變體络凿。 這些變體并行高效運(yùn)行并生成 ConcurrentHashMap 實(shí)例。還有一個(gè)很少使用的 grouping 的相近【的方法】叫做 partitioningBy昂羡。代替分類器方法絮记,它接收一個(gè)謂詞(predicate)并返回鍵為布爾值的 map。此方法有兩個(gè)重載【版本】虐先,其中一個(gè)除謂詞之外還包含下游收集器怨愤。通過(guò) counting 方法返回的收集器僅用作下游收集器。通過(guò) count 方法直接在 Stream 上提供相同的功能蛹批,因此沒(méi)有理由說(shuō) collect(counting())( there is never a reason to say collect(counting())) 撰洗。此屬性還有十五種收集器方法。它們包括九個(gè)方法腐芍,其名稱以 summing差导,averaging 和 summarizing 開頭(其功能在相應(yīng)的基本類型流上可用)。它們還包括 reducing 方法的所有重載甸赃,以及 filter柿汛,mapping,flatMapping 和 collectingAndThen 方法。大多數(shù)程序猿可以安心地忽略大多數(shù)這種方法络断。從設(shè)計(jì)角度來(lái)看裁替,這些收集器代表了嘗試在收集器中部分復(fù)制流的功能,以便下游收集器可以充當(dāng)“迷你流(ministreams)”貌笨。

??我們還有三種 Collectors 方法尚未提及弱判。雖然他們?cè)?Collectors 里面,但他們不涉及集合锥惋。前兩個(gè)是 minBy 和 maxBy昌腰,它們?nèi)”容^器并返回由比較器確定的流中的最小或最大元素。它們是 Stream 接口中 min 和 max 方法的小擴(kuò)展【簡(jiǎn)單的實(shí)現(xiàn)】膀跌,是 BinaryOperator 中類似命名方法返回的二元運(yùn)算符的收集器類似物遭商。回想一下捅伤,我們?cè)谧顣充N專輯的例子中使用了 BinaryOperator.maxBy劫流。

??最后的 Collectors 方法是 join,它只對(duì) CharSequence 實(shí)例的流進(jìn)行操作丛忆,例如字符串祠汇。 在其無(wú)參數(shù)形式中,它返回一個(gè)簡(jiǎn)單地連接元素的收集器熄诡。它的一個(gè)參數(shù)形式采用名為 delimiter 的單個(gè) CharSequence 參數(shù)可很,并返回一個(gè)連接流元素的收集器,在相鄰元素之間插入分隔符凰浮。如果傳入逗號(hào)作為分隔符我抠,則收集器將返回逗號(hào)分隔值字符串(但請(qǐng)注意,如果流中的任何元素包含逗號(hào)导坟,則字符串將不明確)屿良。除了分隔符之外,三個(gè)參數(shù)形式還帶有前綴和后綴惫周。生成的收集器會(huì)生成類似于打印集合時(shí)獲得的字符串尘惧,例如[came, saw, conquered]。

??總之递递,流管道編程的本質(zhì)是無(wú)副作用的功能對(duì)象喷橙。這適用于傳遞給流和相關(guān)對(duì)象的幾乎所有的函數(shù)對(duì)象(This applies to all of the many function objects passed to streams and related objects)。終端操作 forEach 僅應(yīng)用于報(bào)告流執(zhí)行的計(jì)算結(jié)果登舞,而不是用于執(zhí)行計(jì)算贰逾。為了正確使用流,你必須了解收集器菠秒。最重要的收集器工廠是 toList疙剑,toSet氯迂,toMap,groupingBy 和 join言缤。

47.Stream 要優(yōu)先用 Collection 作為返回類型

??許多方法返回元素序列嚼蚀。在 Java 8 之前,這些方法的返回類型是集合的接口 Collection管挟,Set 和 List;Iterable;和數(shù)組類型轿曙。通常,很容易決定返回哪些類型僻孝。準(zhǔn)確來(lái)說(shuō)是一個(gè)集合接口导帝。如果該方法僅用于 for-each 循環(huán)或返回的序列無(wú)法實(shí)現(xiàn)某些 Collection 方法(通常為 contains(Object)),則使用 Iterable 接口穿铆。如果返回的元素是基本類型值或者存在嚴(yán)格的性能要求您单,則使用數(shù)組。在 Java 8 中荞雏,流被添加到 java 庫(kù)中睹限,這使得為返回序列的方法選擇恰當(dāng)?shù)姆祷仡愋偷娜蝿?wù)變得非常復(fù)雜。

??你可能聽說(shuō)過(guò)讯檐,流現(xiàn)在是返回一系列元素的公認(rèn)選擇,正如第 45 項(xiàng)所描述的染服,流不會(huì)使迭代過(guò)時(shí):編寫好的代碼需要適當(dāng)?shù)亟M合流和遍歷别洪。如果 API 只返回一個(gè)流,而某些用戶想要使用 for-each 循環(huán)遍歷返回的序列柳刮,那么這些用戶理所當(dāng)然會(huì)感到不安挖垛。特別令人沮喪的是,Stream 接口包含 Iterable 接口中唯一的抽象方法秉颗,Stream 的此方法規(guī)范與 Iterable 兼容痢毒。

??可悲的是,這個(gè)問(wèn)題沒(méi)有好的解決方法蚕甥。乍一看哪替,似乎可以將方法引用傳遞給 Stream 的迭代器方法。結(jié)果代碼可能有點(diǎn)嘈雜和模糊菇怀,但并非不合理:

// Won't compile, due to limitations on Java's type inference
for (ProcessHandle ph : ProcessHandle.allProcesses()::iterator) {
    // Process the process
}

??不幸的是凭舶,如果你嘗試編譯此代碼,你將收到一條錯(cuò)誤消息:

Test.java:6: error: method reference not expected here
for (ProcessHandle ph : ProcessHandle.allProcesses()::iterator) {
                        ^

??為了使代碼編譯爱沟,你必須將方法引用強(qiáng)制轉(zhuǎn)換為適合參數(shù)化的 Iterable:

// Hideous workaround to iterate over a stream
for (ProcessHandle ph : (Iterable<ProcessHandle>) ProcessHandle.allProcesses()::iterator)

??此客戶端代碼有效帅霜,但在實(shí)踐中使用它太嘈雜和模糊。更好的解決方法是使用適配器方法呼伸。JDK 沒(méi)有提供這樣的方法身冀,但是使用上面的代碼片段中相同的技術(shù),可以很容易地編寫一個(gè)方法。請(qǐng)注意搂根,在適配器方法中不需要強(qiáng)制轉(zhuǎn)換珍促,因?yàn)?Java 類型推斷在此上下文中正常工作:

// Adapter from Stream<E> to Iterable<E>
public static <E> Iterable<E> iterableOf(Stream<E> stream) {
    return stream::iterator;
}

??使用此適配器,你可以使用 for-each 語(yǔ)句迭代任何流:

for (ProcessHandle p : iterableOf(ProcessHandle.allProcesses())) {
    // Process the process
}

??請(qǐng)注意兄墅,第 34 項(xiàng)中的 Anagrams 程序的流版本使用 Files.lines 方法讀取字典踢星,而迭代版本使用 scanner。Files.lines 方法優(yōu)于 scanner隙咸,它可以在讀取文件時(shí)悄悄地處理(silently swallows)任何異常沐悦。理想情況下,我們也會(huì)在迭代版本中使用 Files.lines五督。如果 API 僅提供對(duì)序列的流的訪問(wèn)并且他們希望使用 for-each 語(yǔ)句遍歷序列藏否,那么程序員將會(huì)做出這種折中的方法【在迭代的版本中使用 Files.lines】。

??相反充包,想要使用流管道處理序列的程序猿理所當(dāng)然會(huì)因?yàn)?API 僅提供 Iterable 而感到難過(guò)【傲嬌】副签。再提一次 JDK 沒(méi)有提供適配器,但編寫一個(gè)是很容易的:

// Adapter from Iterable<E> to Stream<E>
public static <E> Stream<E> streamOf(Iterable<E> iterable) {
    return StreamSupport.stream(iterable.spliterator(), false);
}

??如果你正在編寫一個(gè)返回一系列對(duì)象的方法基矮,并且你知道它只會(huì)在流管道中使用淆储,那么你當(dāng)然可以隨意返回一個(gè)流。類似地家浇,返回僅用于遍歷的序列的方法應(yīng)返回 Iterable本砰。但是,如果你正在編寫一個(gè)返回序列的公共 API钢悲,那么你應(yīng)該為想要編寫流管道的用戶以及想要編寫 for-each 語(yǔ)句的用戶提供服務(wù)点额。除非你有充分的理由相信【使用該 API 的】大多數(shù)用戶希望使用相同的機(jī)制。

??Collection 接口是 Iterable 的子類型莺琳,并且具有 stream 方法还棱,因此它提供迭代和流訪問(wèn)。因此惭等,Collection 或適當(dāng)?shù)淖宇愋屯ǔJ枪残蛄蟹祷胤椒ǖ淖罴逊祷仡愋?/strong>珍手。 Arrays 還提供了 Arrays.asList 和 Stream.of 方法的簡(jiǎn)單遍歷和流訪問(wèn)。如果你返回的序列小到足以容易地放入內(nèi)存中辞做,那么最好返回一個(gè)標(biāo)準(zhǔn)的集合實(shí)現(xiàn)珠十,例如 ArrayList 或 HashSet。但是不要在內(nèi)存中存儲(chǔ)大的序列而只是為了將它作為集合返回凭豪。

??如果你返回的序列很大但可以簡(jiǎn)潔地表示焙蹭,請(qǐng)考慮實(shí)現(xiàn)一個(gè)特殊用途的集合。例如嫂伞,假設(shè)你要返回給定集的冪集(power set)孔厉,該集包含其所有子集拯钻。{a斧吐,b朗儒,c}的冪集為{{}棕叫,{a}爷狈,甜攀,{c}饼记,{a新荤,b}凡橱,{a小作,c},{b稼钩,c}顾稀,{a,b坝撑,c}}静秆。如果一個(gè)集合具有 n 個(gè)元素,則其冪集具有 2^n 個(gè)巡李。因此抚笔,你甚至不應(yīng)該考慮將冪集存儲(chǔ)在標(biāo)準(zhǔn)集合的實(shí)現(xiàn)中。但是侨拦,在 AbstractList 的幫助下塔沃,很容易為此實(shí)現(xiàn)自定義集合。

??技巧是使用冪集中每個(gè)元素的索引作為位向量阳谍,其中索引中的第 n 位表示源集合中是否存在第 n 個(gè)元素。本質(zhì)上螃概,從 0 到 2^n - 1 的二進(jìn)制數(shù)和 n 個(gè)元素集的冪集之間存在自然映射矫夯。以下是代碼:

// Returns the power set of an input set as custom collection
public class PowerSet {
    public static final <E> Collection<Set<E>> of(Set<E> s) {
        List<E> src = new ArrayList<>(s);
        if (src.size() > 30)
            throw new IllegalArgumentException("Set too big " + s);
        return new AbstractList<Set<E>>() {
            @Override public int size() {
                return 1 << src.size(); // 2 to the power srcSize
            }
            @Override public boolean contains(Object o) {
                return o instanceof Set && src.containsAll((Set)o);
            }
            @Override public Set<E> get(int index) {
            Set<E> result = new HashSet<>();
                for (int i = 0; index != 0; i++, index >>= 1)
                    if ((index & 1) == 1)
                        result.add(src.get(i));
                return result;
            }
        };
    }
}

??請(qǐng)注意,如果輸入集具有超過(guò) 30 個(gè)元素吊洼,則 PowerSet.of 會(huì)拋出異常训貌。這突出了使用 Collection 作為返回類型的缺點(diǎn)(而 Stream 或 Iterable 沒(méi)有該缺點(diǎn)):Collection 具有 int 返回大小的方法,該方法將返回序列的長(zhǎng)度冒窍,限制為 Integer.MAX_VALUE 或 2^31-1递沪。如果集合更大,甚至無(wú)限综液,Collection 規(guī)范允許 size 方法返回 2^31-1款慨,但這不是一個(gè)完全令人滿意的解決方案。

??為了在 AbstractCollection 上編寫 Collection 實(shí)現(xiàn)谬莹,你只需要實(shí)現(xiàn) Iterable 所需的兩個(gè)方法:contains 和 size檩奠。通常桩了,編寫這些方法的有效實(shí)現(xiàn)是很容易的。如果不可行埠戳,可能是因?yàn)樵诘l(fā)生之前無(wú)法預(yù)先確定序列的內(nèi)容井誉,返回流或可迭代的【結(jié)果】,哪種感覺(jué)起來(lái)更自然就返回哪種整胃。如果你要選擇的話颗圣,你可以使用兩種不同的方法將兩種類型都返回。

??有時(shí)你會(huì)根據(jù)實(shí)施方式的難易程度選擇返回類型屁使。例如在岂,假設(shè)你要編寫一個(gè)返回輸入列表的所有(連續(xù))子列表的方法。生成這些子列表只需要三行代碼并將它們放在標(biāo)準(zhǔn)集合中屋灌,但保存此集合所需的內(nèi)存是源列表大小的二次方洁段。雖然這并不像指數(shù)級(jí)的冪集那么糟糕,但顯然是不可接受的共郭。正如我們?yōu)閮缂龅哪菢屿羲浚瑢?shí)現(xiàn)自定義集合將是冗長(zhǎng)的,因?yàn)?JDK 缺乏 Iterator 框架實(shí)現(xiàn)來(lái)幫助我們除嘹。

??但是【我們可以】直接實(shí)現(xiàn)輸入列表的所有子列表的流写半,盡管它確實(shí)需要一些洞察力。讓我們調(diào)用一個(gè)子列表尉咕,該子列表包含列表的第一個(gè)元素和列表的前綴(prefix)叠蝇。例如,(a年缎,b悔捶,c)的前綴是(a),(a单芜,b)和(a蜕该,b,c)洲鸠。 類似地堂淡,讓我們調(diào)用包含后綴的最后一個(gè)元素的子列表,因此(a扒腕,b绢淀,c)的后綴是(a,b瘾腰,c)皆的,(b,c)和(c)蹋盆。洞察的點(diǎn)就是列表的子列表只是前綴的后綴(或相同的后綴的前綴)和空列表祭务。通過(guò)這個(gè)觀點(diǎn)直接就可以有了清晰内狗、合理簡(jiǎn)潔的實(shí)施方案:

// Returns a stream of all the sublists of its input list
public class SubLists {
    public static <E> Stream<List<E>> of(List<E> list) {
        return Stream.concat(Stream.of(Collections.emptyList()), prefixes(list).flatMap(SubLists::suffixes));
    }
    private static <E> Stream<List<E>> prefixes(List<E> list) {
        return IntStream.rangeClosed(1, list.size()).mapToObj(end -> list.subList(0, end));
    }
    private static <E> Stream<List<E>> suffixes(List<E> list) {
        return IntStream.range(0, list.size()).mapToObj(start -> list.subList(start, list.size()));
    }
}

??請(qǐng)注意,Stream.concat 方法用于將空列表添加到返回的流中义锥。另請(qǐng)注意柳沙,flatMap 方法(第 45 項(xiàng))用于生成由所有前綴的所有后綴組成的單個(gè)流。最后拌倍,請(qǐng)注意我們通過(guò)映射 IntStream.range 和 IntStream.rangeClosed 返回的連續(xù) int 值的流來(lái)生成前綴和后綴赂鲤。簡(jiǎn)單地說(shuō),這個(gè)習(xí)慣用法是整數(shù)索引上標(biāo)準(zhǔn) for 循環(huán)的流等價(jià)物( This idiom is, roughly speaking, the stream equivalent of the standard for-loop on integer indices)柱恤。因此数初,我們的子列表實(shí)現(xiàn)的思想類似明顯的嵌套 for 循環(huán):

for (int start = 0; start < src.size(); start++)
    for (int end = start + 1; end <= src.size(); end++)
        System.out.println(src.subList(start, end));

??可以將此 for 循環(huán)直接轉(zhuǎn)換為流。結(jié)果比我們之前的實(shí)現(xiàn)更簡(jiǎn)潔梗顺,但可讀性稍差泡孩。它的思想類似第 45 項(xiàng)中笛卡爾積的流代碼:

// Returns a stream of all the sublists of its input list
public static <E> Stream<List<E>> of(List<E> list) {
    return IntStream.range(0, list.size())
        .mapToObj(start -> IntStream.rangeClosed(start + 1, list.size())
        .mapToObj(end -> list.subList(start, end)))
        .flatMap(x -> x);
}

??與之前的 for 循環(huán)一樣,此代碼不會(huì)產(chǎn)生(emit)空列表寺谤。為了解決這個(gè)問(wèn)題仑鸥,你可以使用 concat,就像我們?cè)谥鞍姹局兴龅哪菢颖淦ǎ蛘咴?rangeClosed 調(diào)用中用(int)Math.signum(start)替換 1眼俊。

??這些子列表的流實(shí)現(xiàn)中的任何一個(gè)都很好,但兩者都需要用戶使用一些 Stream-to-Iterable 適配器或在迭代更自然的地方使用流粟关。Stream-to-Iterable 適配器不僅使客戶端代碼混亂疮胖,而且還會(huì)使我的機(jī)器上的循環(huán)速度降低 2.3 倍。專用的 Collection 實(shí)現(xiàn)(此處未顯示)相當(dāng)冗長(zhǎng)闷板,但運(yùn)行速度是我機(jī)器上基于流的實(shí)現(xiàn)的 1.4 倍澎灸。

??總之,在編寫返回元素序列的方法時(shí)遮晚,請(qǐng)記住性昭,你的某些用戶可能希望將它們作為流進(jìn)行處理,而其他用戶可能希望使用它們進(jìn)行遍歷鹏漆。盡量適應(yīng)這兩個(gè)群體。如果返回集合是可行的创泄,那么就返回集合艺玲。如果你已經(jīng)擁有集合中的元素,或者序列中的元素?cái)?shù)量很小足以證明創(chuàng)建新元素是正確的鞠抑,那么就返回標(biāo)準(zhǔn)集合饭聚,例如 ArrayList。否則搁拙,請(qǐng)考慮實(shí)現(xiàn)自定義的集合秒梳,就像我們?yōu)閮缂龅哪菢臃唷H绻祷丶鲜遣豢尚械模瑒t返回一個(gè)流或可迭代的【類型】酪碘,無(wú)論哪個(gè)看起來(lái)更自然朋譬。如果在將來(lái)的 Java 版本中,Stream 接口聲明被修改為擴(kuò)展(extend)Iterable兴垦,那么你應(yīng)該隨意返回流徙赢,因?yàn)樗鼈儗⒃试S進(jìn)行流處理和遍歷。

48.謹(jǐn)慎使用 Stream 并行

??在主流語(yǔ)言中探越,在提供便于并發(fā)編程任務(wù)功能方面狡赐,Java 始終處于最前沿【的位置】(Among mainstream languages, Java has always been at the forefront of providing facilities to ease the task of concurrent programming)。當(dāng) Java 于 1996 年發(fā)布時(shí)钦幔,它內(nèi)置了對(duì)線程的支持枕屉,具有同步和等待/通知【的功能】(When Java was released in 1996, it had built-in support for threads, with synchronization and wait/notify)。Java 5 引入了 java.util.concurrent 庫(kù)鲤氢,包含并發(fā)集合和執(zhí)行器框架搀擂。 Java 7 引入了 fork-join 包,這是一個(gè)用于并行分解(parallel decomposition)的高性能框架铜异。Java 8 引入了流哥倔,可以通過(guò)對(duì)并行方法的單個(gè)調(diào)用來(lái)并行化。用 Java 編寫并發(fā)程序變得越來(lái)越容易揍庄,但編寫正確快速的并發(fā)程序就跟以前一樣困難咆蒿。安全性和活性違規(guī)(liveness violations )是并發(fā)編程中的事實(shí),并行流管道也不例外蚂子。

??考慮第 45 項(xiàng)中的這個(gè)程序:

// Stream-based program to generate the first 20 Mersenne primes
public static void main(String[] args) {
    primes().map(p -> TWO.pow(p.intValueExact()).subtract(ONE))
        .filter(mersenne -> mersenne.isProbablePrime(50))
        .limit(20)
        .forEach(System.out::println);
}
static Stream<BigInteger> primes() {
    return Stream.iterate(TWO, BigInteger::nextProbablePrime);
}

??在我的機(jī)器上沃测,該程序立即開始打印質(zhì)數(shù),并需要 12.5 秒才能完成運(yùn)行食茎。假設(shè)我試圖通過(guò)向流管道添加對(duì) parallel()的調(diào)用來(lái)加速它蒂破。你認(rèn)為它的表現(xiàn)會(huì)怎樣?它【的運(yùn)行速度】會(huì)加快幾個(gè)百分點(diǎn)嗎别渔?還是慢幾個(gè)百分點(diǎn)附迷?可悲的是,發(fā)生的事情是它沒(méi)有打印任何東西哎媚,但是 CPU 使用率飆升至 90%并且無(wú)限期地停留在那里(活性失敗(liveness failure))喇伯。該程序最終可能會(huì)終止,但我不愿意去發(fā)現(xiàn)【等待這個(gè)結(jié)果】; 半小時(shí)后我強(qiáng)行停止【了程序】拨与。

??這里發(fā)生了什么稻据?簡(jiǎn)而言之,流的庫(kù)不知道如何并行化此管道并且試探啟動(dòng)(heuristics)失敗买喧。即使在最好的情況下捻悯,如果源來(lái)自 Stream.iterate匆赃,或者使用中間操作限制,并行化管道也不太可能提高其性能(parallelizing a pipeline is unlikely to increase its performance if the source is from Stream.iterate, or the intermediate operation limit is used.)今缚。這條管道必須應(yīng)對(duì)這兩個(gè)問(wèn)題算柳。更糟糕的是,默認(rèn)的并行化策略是通過(guò)假設(shè)處理一些額外元素并丟棄任何不需要的結(jié)果不會(huì)帶來(lái)任何損失的前提下來(lái)處理限制的不可預(yù)測(cè)性荚斯。在這種情況下埠居,找到每個(gè)梅森質(zhì)數(shù)需要大約兩倍的時(shí)間才能找到前一個(gè)。因此事期,計(jì)算單個(gè)額外元素的成本大致等于計(jì)算所有先前元素組合的成本滥壕,并且這種看起來(lái)沒(méi)什么損失的管道會(huì)使自動(dòng)并行化算法癱瘓。這個(gè)故事的寓意很簡(jiǎn)單:不要不加選擇的地使用并行化流兽泣。導(dǎo)致的性能后果可能是災(zāi)難性的绎橘。

??并行性的性能增益最好是在 ArrayList,HashMap唠倦,HashSet 和 ConcurrentHashMap 實(shí)例上称鳞;int 數(shù)組;和 long 數(shù)組(performance gains from parallelism are best on streams over ArrayList, HashMap, HashSet, and ConcurrentHashMap instances; arrays; int ranges; and long ranges)稠鼻,將這作為一項(xiàng)規(guī)則冈止。這些數(shù)據(jù)結(jié)構(gòu)的共同之處在于它們都可以準(zhǔn)確且分成任何所需大小的子范圍的代價(jià)是很小的,這使得在并行線程之間劃分工作變得容易候齿。流庫(kù)用于執(zhí)行此任務(wù)的抽象是 spliterator熙暴,它由 Stream 和 Iterable 上的 spliterator 方法返回。

??所有這些數(shù)據(jù)結(jié)構(gòu)的另一個(gè)重要因素是它們?cè)陧樞蛱幚頃r(shí)提供了非常好的位置引用(locality of reference):元素的順序和【元素的】引用一起存儲(chǔ)在存儲(chǔ)器中慌盯。這些引用所引用的對(duì)象在存儲(chǔ)器中可能彼此不接近周霉,這減少了位置引用(The objects referred to by those references may not be close to one another in memory, which reduces locality-of-reference.)。對(duì)于并行化操作而言亚皂,位置引用非常重要:如果沒(méi)有位置引用俱箱,線程大部分時(shí)間會(huì)處在空閑狀態(tài),等待數(shù)據(jù)從內(nèi)存?zhèn)鬏數(shù)教幚砥鞯木彺婷鸨亍>哂凶罴盐恢靡玫臄?shù)據(jù)結(jié)構(gòu)是原始數(shù)組狞谱,因?yàn)閿?shù)據(jù)本身連續(xù)存儲(chǔ)在存儲(chǔ)器中。

??流管道終端操作的本質(zhì)也會(huì)影響并行執(zhí)行的有效性禁漓。如果與管道的整體工作相比在終端操作中完成了大量工作并且該操作本質(zhì)上是按順序的跟衅,那么并行化管道的有效性是受限的。并行性最佳的終端操作是減少(reductions)璃饱,其中從管道中出現(xiàn)的所有元素使用 Stream 的 reduce 方法或減少預(yù)打包(prepackaged reductions)(例如 min与斤,max肪康,count 和 sum)進(jìn)行組合荚恶。短路操作(shortcircuiting)anyMatch撩穿,allMatch 和 noneMatch 也適用于并行操作。Stream 的 collect 方法執(zhí)行的操作(稱為可變約簡(jiǎn)( mutable reductions))不是并行性的良好選擇谒撼,因?yàn)榻M合集合的開銷是很昂貴的食寡。

??如果你編寫自己的 Stream,Iterable 或 Collection 實(shí)現(xiàn)并且希望獲得良好的并行性能廓潜,則必須覆蓋 spliterator 方法并廣泛測(cè)試生成的流的并行性能抵皱。編寫高質(zhì)量的 spliterators 是很困難的,超出了本書的范圍辩蛋。

??并行化流不僅會(huì)導(dǎo)致性能不佳呻畸,包括活性失敗; 它可能導(dǎo)致不正確的結(jié)果和不可預(yù)測(cè)的行為(安全性失敗)悼院。使用映射器伤为,過(guò)濾器和其他程序員提供的不符合其規(guī)范的功能對(duì)象的管道并行化可能會(huì)導(dǎo)致安全性失敗。Stream 規(guī)范對(duì)這些功能對(duì)象提出了嚴(yán)格的要求据途。例如绞愚,傳遞給 Stream 的 reduce 操作的累加器和組合器函數(shù)必須是關(guān)聯(lián)的,非侵入的和無(wú)狀態(tài)的颖医。如果你違反了這些要求(其中一些在第 46 項(xiàng)中討論過(guò))位衩,但按順序運(yùn)行你的管道,則可能會(huì)產(chǎn)生正確的結(jié)果; 如果你將它并行化熔萧,它可能會(huì)失敗糖驴,也許是災(zāi)難性的。

??沿著這些思路哪痰,值得注意的是遂赠,即使并行化的梅森素?cái)?shù)程序已經(jīng)完成,它也不會(huì)以正確的(升序)順序打印素?cái)?shù)晌杰。要保留順序版本顯示的順序跷睦,你必須使用 forEachOrdered 替換 forEach 終端操作,該操作保證以相遇順序(encounter order)遍歷并行流肋演。

??即使假設(shè)你正在使用有效可拆分的源流(帶有一個(gè)并行化或代價(jià)低的終端操作)和非侵入(non-interfering)的函數(shù)對(duì)象抑诸,你無(wú)法從并行化中獲得很好的加速效果,除非管道做了足夠的實(shí)際工作來(lái)抵消使用并行化相關(guān)的成本(unless the pipeline is doing enough real work to offset the costs associated with parallelism)爹殊。作個(gè)非常粗略的估計(jì)蜕乡,流中元素的數(shù)量乘以每個(gè)元素執(zhí)行的代碼行數(shù)應(yīng)該至少為十萬(wàn)[Lea14]。

??重要的是要記住并行化流是嚴(yán)格的性能優(yōu)化梗夸。與任何優(yōu)化一樣层玲,你必須在更改之前和之后測(cè)試性能,以確保它【的優(yōu)化是】值得做【的】(第 67 項(xiàng))。理想情況下辛块,你應(yīng)該在實(shí)際的系統(tǒng)設(shè)置中執(zhí)行測(cè)試畔派。通常,程序中的所有并行流管道都在公共 fork-join 線程池中運(yùn)行润绵。單個(gè)行為不當(dāng)?shù)墓艿揽赡軙?huì)影響系統(tǒng)中其他不相關(guān)部分的行為线椰。

??聽起來(lái)使用流并行會(huì)一直在違背你的意愿,它們確實(shí)是這樣的(If it sounds like the odds are stacked against you when parallelizing stream pipelines, it’s because they are.)尘盼。那些維護(hù)數(shù)百萬(wàn)行代碼的人大量使用流憨愉,只發(fā)現(xiàn)了在很少數(shù)的地方使用并行流是有效地。這并不意味著你應(yīng)該避免并行化流卿捎。在適當(dāng)?shù)那闆r下配紫,只需通過(guò)向流管道添加并行調(diào)用,就可以實(shí)現(xiàn)處理器內(nèi)核數(shù)量的近線性(near-linear)加速午阵。某些領(lǐng)域笨蚁,例如機(jī)器學(xué)習(xí)和數(shù)據(jù)處理,特別適合這些加速趟庄。

??作為并行性有效的流管道的一個(gè)簡(jiǎn)單示例括细,請(qǐng)考慮此函數(shù)來(lái)計(jì)算 π(n),素?cái)?shù)小于或等于 n:

// Prime-counting stream pipeline - benefits from parallelization
static long pi(long n) {
    return LongStream.rangeClosed(2, n)
        .mapToObj(BigInteger::valueOf)
        .filter(i -> i.isProbablePrime(50))
        .count();
}

??在我的機(jī)器上戚啥,使用此功能計(jì)算 π(10^8)需要 31 秒奋单。 只需添加 parallel()調(diào)用即可將時(shí)間縮短為 9.2 秒:

// Prime-counting stream pipeline - parallel version
static long pi(long n) {
    return LongStream.rangeClosed(2, n)
        .parallel()
        .mapToObj(BigInteger::valueOf)
        .filter(i -> i.isProbablePrime(50))
        .count();
}

??換句話說(shuō),并行化計(jì)算可以在我的四核機(jī)器上將其加速 3.7 倍猫十。 值得注意的是览濒,這并不是你在實(shí)踐中如何計(jì)算大 n 值的 π(n)。有更高效的算法拖云,特別是 Lehmer 的公式贷笛。

??如果要并行化隨機(jī)數(shù)流,請(qǐng)從 SplittableRandom 實(shí)例開始宙项,而不是 ThreadLocalRandom(或基本上過(guò)時(shí)的 Random)乏苦。SplittableRandom 是專門為此而設(shè)計(jì)的,具有線性加速的潛力尤筐。ThreadLocalRandom 設(shè)計(jì)用于單個(gè)線程汇荐,并將自適應(yīng)為并行流的源,但不會(huì)像 SplittableRandom 一樣快盆繁。隨機(jī)同步每個(gè)操作掀淘,因此會(huì)導(dǎo)致過(guò)度(近似殺戮)的爭(zhēng)搶(so it will result in excessive, parallelism-killing contention)【意思應(yīng)該是導(dǎo)致的資源爭(zhēng)搶會(huì)很激烈】。

??總之油昂,除非你有充分的理由相信它將保持計(jì)算的正確性并提高其速度革娄,否則就不應(yīng)該嘗試并行化流管道倾贰。不恰當(dāng)?shù)夭⑿谢鞯某杀究赡苁浅绦蚴』蛐阅転?zāi)難。如果你認(rèn)為并行性可能是合理的拦惋,請(qǐng)確保在并行運(yùn)行時(shí)代碼保持【運(yùn)行結(jié)果的】正確躁染,并在實(shí)際條件下進(jìn)行詳細(xì)的性能測(cè)試。如果你的代碼仍然正確并且這些實(shí)驗(yàn)證明你對(duì)性能提升的猜疑架忌,那么才能在生產(chǎn)環(huán)境的代碼中使用并行化流(If your code remains correct and these experiments bear out your suspicion of increased performance, then and only then parallelize the stream in production code.)。

參考鏈接

https://gitee.com/lin-mt/effective-java-third-edition


Kotlin開發(fā)者社區(qū)

專注分享 Java我衬、 Kotlin叹放、Spring/Spring Boot、MySQL挠羔、redis井仰、neo4j、NoSQL破加、Android俱恶、JavaScript、React范舀、Node合是、函數(shù)式編程、編程思想锭环、"高可用聪全,高性能,高實(shí)時(shí)"大型分布式系統(tǒng)架構(gòu)設(shè)計(jì)主題辅辩。

High availability, high performance, high real-time large-scale distributed system architecture design难礼。

分布式框架:Zookeeper、分布式中間件框架等
分布式存儲(chǔ):GridFS玫锋、FastDFS蛾茉、TFS、MemCache撩鹿、redis等
分布式數(shù)據(jù)庫(kù):Cobar谦炬、tddl、Amoeba节沦、Mycat
云計(jì)算吧寺、大數(shù)據(jù)、AI算法
虛擬化散劫、云原生技術(shù)
分布式計(jì)算框架:MapReduce稚机、Hadoop、Storm获搏、Flink等
分布式通信機(jī)制:Dubbo赖条、RPC調(diào)用失乾、共享遠(yuǎn)程數(shù)據(jù)、消息隊(duì)列等
消息隊(duì)列MQ:Kafka纬乍、MetaQ碱茁,RocketMQ
怎樣打造高可用系統(tǒng):基于硬件、軟件中間件仿贬、系統(tǒng)架構(gòu)等一些典型方案的實(shí)現(xiàn):HAProxy纽竣、基于Corosync+Pacemaker的高可用集群套件中間件系統(tǒng)
Mycat架構(gòu)分布式演進(jìn)
大數(shù)據(jù)Join背后的難題:數(shù)據(jù)、網(wǎng)絡(luò)茧泪、內(nèi)存和計(jì)算能力的矛盾和調(diào)和
Java分布式系統(tǒng)中的高性能難題:AIO蜓氨,NIO,Netty還是自己開發(fā)框架队伟?
高性能事件派發(fā)機(jī)制:線程池模型穴吹、Disruptor模型等等。嗜侮。港令。

合抱之木,生于毫末锈颗;九層之臺(tái)顷霹,起于壘土;千里之行击吱,始于足下泼返。不積跬步,無(wú)以至千里姨拥;不積小流绅喉,無(wú)以成江河。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末叫乌,一起剝皮案震驚了整個(gè)濱河市柴罐,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌憨奸,老刑警劉巖革屠,帶你破解...
    沈念sama閱讀 218,858評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異排宰,居然都是意外死亡似芝,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門板甘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)党瓮,“玉大人,你說(shuō)我怎么就攤上這事盐类∧椋” “怎么了呛谜?”我有些...
    開封第一講書人閱讀 165,282評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)枪萄。 經(jīng)常有香客問(wèn)我隐岛,道長(zhǎng),這世上最難降的妖魔是什么瓷翻? 我笑而不...
    開封第一講書人閱讀 58,842評(píng)論 1 295
  • 正文 為了忘掉前任聚凹,我火速辦了婚禮,結(jié)果婚禮上齐帚,老公的妹妹穿的比我還像新娘妒牙。我一直安慰自己,他們只是感情好童谒,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著沪羔,像睡著了一般饥伊。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蔫饰,一...
    開封第一講書人閱讀 51,679評(píng)論 1 305
  • 那天琅豆,我揣著相機(jī)與錄音,去河邊找鬼篓吁。 笑死茫因,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的杖剪。 我是一名探鬼主播冻押,決...
    沈念sama閱讀 40,406評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼盛嘿!你這毒婦竟也來(lái)了洛巢?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,311評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤次兆,失蹤者是張志新(化名)和其女友劉穎稿茉,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體芥炭,經(jīng)...
    沈念sama閱讀 45,767評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡漓库,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了园蝠。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片渺蒿。...
    茶點(diǎn)故事閱讀 40,090評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖彪薛,靈堂內(nèi)的尸體忽然破棺而出蘸嘶,到底是詐尸還是另有隱情良瞧,我是刑警寧澤,帶...
    沈念sama閱讀 35,785評(píng)論 5 346
  • 正文 年R本政府宣布训唱,位于F島的核電站褥蚯,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏况增。R本人自食惡果不足惜赞庶,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望澳骤。 院中可真熱鬧歧强,春花似錦、人聲如沸为肮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)颊艳。三九已至茅特,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間棋枕,已是汗流浹背白修。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留重斑,地道東北人兵睛。 一個(gè)月前我還...
    沈念sama閱讀 48,298評(píng)論 3 372
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像窥浪,于是被迫代替她去往敵國(guó)和親祖很。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評(píng)論 2 355