1. 程式人生 > >Effective Java 第三版——46. 優先考慮流中無副作用的函數

Effective Java 第三版——46. 優先考慮流中無副作用的函數

strong gem sort 集合類 for 成員 流轉 iter 指定

Tips
《Effective Java, Third Edition》一書英文版已經出版,這本書的第二版想必很多人都讀過,號稱Java四大名著之一,不過第二版2009年出版,到現在已經將近8年的時間,但隨著Java 6,7,8,甚至9的發布,Java語言發生了深刻的變化。
在這裏第一時間翻譯成中文版。供大家學習分享之用。
書中的源代碼地址:https://github.com/jbloch/effective-java-3e-source-code
註意,書中的有些代碼裏方法是基於Java 9 API中的,所以JDK 最好下載 JDK 9以上的版本。但是Java 9 只是一個過渡版本,所以建議安裝JDK 10。

技術分享圖片

46. 優先考慮流中無副作用的函數

如果你是一個剛開始使用流的新手,那麽很難掌握它們。僅僅將計算表示為流管道是很困難的。當你成功時,你的程序將運行,但對你來說可能沒有意識到任何好處。流不僅僅是一個API,它是基於函數式編程的範式(paradigm)。為了獲得流提供的可表達性、速度和某些情況下的並行性,你必須采用範式和API。

流範式中最重要的部分是將計算結構化為一系列轉換,其中每個階段的結果盡可能接近前一階段結果的純函數( pure function)。 純函數的結果僅取決於其輸入:它不依賴於任何可變狀態,也不更新任何狀態。 為了實現這一點,你傳遞給流操作的任何函數對象(中間操作和終結操作)都應該沒有副作用。

有時,可能會看到類似於此代碼片段的流代碼,該代碼構建了文本文件中單詞的頻率表:

// 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);

    });

}

這段代碼出了什麽問題? 畢竟,它使用了流,lambdas和方法引用,並得到正確的答案。 簡而言之,它根本不是流代碼; 它是偽裝成流代碼的叠代代碼。 它沒有從流API中獲益,並且它比相應的叠代代碼更長,更難讀,並且更難於維護。 問題源於這樣一個事實:這個代碼在一個終結操作forEach中完成所有工作,使用一個改變外部狀態(頻率表)的lambda。forEach操作除了表示由一個流執行的計算結果外,什麽都不做,這是“代碼中的臭味”,就像一個改變狀態的lambda一樣。那麽這段代碼應該是什麽樣的呢?

// 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。 它更短更清晰。 那麽為什麽有人會用其他方式寫呢? 因為它使用了他們已經熟悉的工具。 Java程序員知道如何使用for-each循環,而forEach終結操作是類似的。 但forEach操作是終端操作中最不強大的操作之一,也是最不友好的流操作。 它是明確的叠代,因此不適合並行化。 forEach操作應僅用於報告流計算的結果,而不是用於執行計算。有時,將forEach用於其他目的是有意義的,例如將流計算的結果添加到預先存在的集合中。

改進後的代碼使用了收集器(collector),這是使用流必須學習的新概念。Collectors的API令人生畏:它有39個方法,其中一些方法有多達5個類型參數。好消息是,你可以從這個API中獲得大部分好處,而不必深入研究它的全部復雜性。對於初學者來說,可以忽略收集器接口,將收集器看作是封裝縮減策略( reduction strategy)的不透明對象。在此上下文中,reduction意味著將流的元素組合為單個對象。 收集器生成的對象通常是一個集合(它代表名稱收集器)。

將流的元素收集到真正的集合中的收集器非常簡單。有三個這樣的收集器:toList()toSet()toCollection(collectionFactory)。它們分別返回集合、列表和程序員指定的集合類型。有了這些知識,我們就可以編寫一個流管道從我們的頻率表中提取出現頻率前10個單詞的列表。

// 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());

註意,我們沒有對toList方法的類收集器進行限定。靜態導入收集器的所有成員是一種慣例和明智的做法,因為它使流管道更易於閱讀

這段代碼中唯一比較棘手的部分是我們把comparing(freq::get).reverse()傳遞給sort方法。comparing是一種比較器構造方法(條目 14),它具有一個key的提取方法。該函數接受一個單詞,而“提取”實際上是一個表查找:綁定方法引用freq::get在frequency表中查找單詞,並返回單詞出現在文件中的次數。最後,我們在比較器上調用reverse方法,因此我們將單詞從最頻繁到最不頻繁進行排序。然後,將流限制為10個單詞並將它們收集到一個列表中就很簡單了。

前面的代碼片段使用Scanner的stream方法在scanner實例上獲取流。這個方法是在Java 9中添加的。如果正在使用較早的版本,可以使用類似於條目 47中(streamOf(Iterable<E>))的適配器將實現了Iterator的scanner序轉換為流。

那麽收集器中的其他36種方法呢?它們中的大多數都是用於將流收集到map中的,這比將流收集到真正的集合中要復雜得多。每個流元素都與一個鍵和一個值相關聯,多個流元素可以與同一個鍵相關聯。

最簡單的映射收集器是toMap(keyMapper、valueMapper),它接受兩個函數,一個將流元素映射到鍵,另一個映射到值。在條目34中的fromString實現中,我們使用這個收集器從enum的字符串形式映射到enum本身:

// 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));

如果流中的每個元素都映射到唯一鍵,則這種簡單的toMap形式是完美的。 如果多個流元素映射到同一個鍵,則管道將以IllegalStateException終止。

toMap更復雜的形式,以及groupingBy方法,提供了處理此類沖突(collisions)的各種方法。一種方法是向toMap方法提供除鍵和值映射器(mappers)之外的merge方法。merge方法是一個BinaryOperator,其中V`是map的值類型。與鍵關聯的任何附加值都使用merge方法與現有值相結合,因此,例如,如果merge方法是乘法,那麽最終得到的結果是是值mapper與鍵關聯的所有值的乘積。

toMap的三個參數形式對於從鍵到與該鍵關聯的選定元素的映射也很有用。例如,假設我們有一系列不同藝術家(artists)的唱片集(albums),我們想要一張從唱片藝術家到最暢銷專輯的map。這個收集器將完成這項工作。

// 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))));

請註意,比較器使用靜態工廠方法maxBy,它是從BinaryOperator靜態導入的。 此方法將Comparator <T>轉換為BinaryOperator <T>,用於計算指定比較器隱含的最大值。 在這種情況下,比較器由比較器構造方法comparing返回,它采用key提取器函數Album :: sales。 這可能看起來有點復雜,但代碼可讀性很好。 簡而言之,它說,“將專輯(albums)流轉換為地map,將每位藝術家(artist)映射到銷售量最佳的專輯。”這與問題陳述出奇得接近。

toMap的三個參數形式的另一個用途是產生一個收集器,當發生沖突時強制執行last-write-wins策略。 對於許多流,結果是不確定的,但如果映射函數可能與鍵關聯的所有值都相同,或者它們都是可接受的,則此收集器的行為可能正是您想要的:

// Collector to impose last-write-wins policy

toMap(keyMapper, valueMapper, (oldVal, newVal) ->newVal)

toMap的第三個也是最後一個版本采用第四個參數,它是一個map工廠,用於指定特定的map實現,例如EnumMapTreeMap

toMap的前三個版本也有變體形式,名為toConcurrentMap,它們並行高效運行並生成ConcurrentHashMap實例。

除了toMap方法之外,Collectors API還提供了groupingBy方法,該方法返回收集器以生成基於分類器函數(classifier function)將元素分組到類別中的map。 分類器函數接受一個元素並返回它所屬的類別。 此類別來用作元素的map的鍵。 groupingBy方法的最簡單版本僅采用分類器並返回一個map,其值是每個類別中所有元素的列表。 這是我們在條目 45中的Anagram程序中使用的收集器,用於生成從按字母順序排列的單詞到單詞列表的map:

Map<String, Long> freq = words

        .collect(groupingBy(String::toLowerCase, counting()));

groupingBy的第三個版本允許指定除downstream收集器之外的map工廠。 請註意,這種方法違反了標準的可伸縮參數列表模式(standard telescoping argument list pattern):mapFactory參數位於downStream參數之前,而不是之後。 此版本的groupingBy可以控制包含的map以及包含的集合,因此,例如,可以指定一個收集器,它返回一個TreeMap,其值是TreeSet

groupingByConcurrent方法提供了groupingBy的所有三個重載的變體。 這些變體並行高效運行並生成ConcurrentHashMap實例。 還有一個很少使用的grouping的親戚稱為partitioningBy。 代替分類器方法,它接受predicate並返回其鍵為布爾值的map。 此方法有兩種重載,除了predicate之外,其中一種方法還需要downstream收集器。

通過counting方法返回的收集器僅用作下遊收集器。 Stream上可以通過count方法直接使用相同的功能,因此沒有理由說collect(counting())。 此屬性還有十五種收集器方法。 它們包括九個方法,其名稱以summingaveragingsummarizing開頭(其功能在相應的原始流類型上可用)。 它們還包括reduce方法的所有重載,以及filtermappingflatMappingcollectingAndThen方法。 大多數程序員可以安全地忽略大多數這些方法。 從設計的角度來看,這些收集器代表了嘗試在收集器中部分復制流的功能,以便下遊收集器可以充當“迷你流(ministreams)”。

我們還有三種收集器方法尚未提及。 雖然他們在收Collectors類中,但他們不涉及集合。 前兩個是minBymaxBy,它們取比較器並返回比較器確定的流中的最小或最大元素。 它們是Stream接口中min和max方法的次要總結,是BinaryOperator中類似命名方法返回的二元運算符的類似收集器。 回想一下,我們在最暢銷的專輯中使用了BinaryOperator.maxBy方法。

最後的Collectors中方法是join,它僅對CharSequence實例(如字符串)的流進行操作。 在其無參數形式中,它返回一個簡單地連接元素的收集器。 它的一個參數形式采用名為delimiter的單個CharSequence參數,並返回一個連接流元素的收集器,在相鄰元素之間插入分隔符。 如果傳入逗號作為分隔符,則收集器將返回逗號分隔值字符串(但請註意,如果流中的任何元素包含逗號,則字符串將不明確)。 除了分隔符之外,三個參數形式還帶有前綴和後綴。 生成的收集器會生成類似於打印集合時獲得的字符串,例如[came, saw, conquered]

總之,編程流管道的本質是無副作用的函數對象。 這適用於傳遞給流和相關對象的所有許多函數對象。 終結操作orEach僅應用於報告流執行的計算結果,而不是用於執行計算。 為了正確使用流,必須了解收集器。 最重要的收集器工廠是toListtoSettoMapgroupingBy和join

Effective Java 第三版——46. 優先考慮流中無副作用的函數