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。
java
若是你是一個剛開始使用流的新手,那麼很難掌握它們。僅僅將計算表示爲流管道是很困難的。當你成功時,你的程序將運行,但對你來講可能沒有意識到任何好處。流不只僅是一個API,它是基於函數式編程的範式(paradigm)。爲了得到流提供的可表達性、速度和某些狀況下的並行性,你必須採用範式和API。git
流範式中最重要的部分是將計算結構化爲一系列轉換,其中每一個階段的結果儘量接近前一階段結果的純函數( pure function)。 純函數的結果僅取決於其輸入:它不依賴於任何可變狀態,也不更新任何狀態。 爲了實現這一點,你傳遞給流操做的任何函數對象(中間操做和終結操做)都應該沒有反作用。程序員
有時,可能會看到相似於此代碼片斷的流代碼,該代碼構建了文本文件中單詞的頻率表:github
// 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意味着將流的元素組合爲單個對象。 收集器生成的對象一般是一個集合(它表明名稱收集器)。app
將流的元素收集到真正的集合中的收集器很是簡單。有三個這樣的收集器: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實現,例如EnumMap
或TreeMap
。
toMap的前三個版本也有變體形式,名爲toConcurrentMap
,它們並行高效運行並生成ConcurrentHashMap
實例。
除了toMap方法以外,Collectors API還提供了grouping
By方法,該方法返回收集器以生成基於分類器函數(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())
。 此屬性還有十五種收集器方法。 它們包括九個方法,其名稱以summing
,averaging
和summarizing
開頭(其功能在相應的原始流類型上可用)。 它們還包括reduce
方法的全部重載,以及filter
,mapping
,flatMapping
和collectingAndThen
方法。 大多數程序員能夠安全地忽略大多數這些方法。 從設計的角度來看,這些收集器表明了嘗試在收集器中部分複製流的功能,以便下游收集器能夠充當「迷你流(ministreams)」。
咱們還有三種收集器方法還沒有說起。 雖然他們在收Collectors類中,但他們不涉及集合。 前兩個是minBy
和maxBy
,它們取比較器並返回比較器肯定的流中的最小或最大元素。 它們是Stream接口中min和max方法的次要總結,是BinaryOperator中相似命名方法返回的二元運算符的相似收集器。 回想一下,咱們在最暢銷的專輯中使用了BinaryOperator.maxBy
方法。
最後的Collectors中方法是join
,它僅對CharSequence
實例(如字符串)的流進行操做。 在其無參數形式中,它返回一個簡單地鏈接元素的收集器。 它的一個參數形式採用名爲delimiter
的單個CharSequence
參數,並返回一個鏈接流元素的收集器,在相鄰元素之間插入分隔符。 若是傳入逗號做爲分隔符,則收集器將返回逗號分隔值字符串(但請注意,若是流中的任何元素包含逗號,則字符串將不明確)。 除了分隔符以外,三個參數形式還帶有前綴和後綴。 生成的收集器會生成相似於打印集合時得到的字符串,例如[came, saw, conquered]
。
總之,編程流管道的本質是無反作用的函數對象。 這適用於傳遞給流和相關對象的全部許多函數對象。 終結操做orEach
僅應用於報告流執行的計算結果,而不是用於執行計算。 爲了正確使用流,必須瞭解收集器。 最重要的收集器工廠是toList
,toSet
,toMap
,groupingBy和join
。