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
許多方法返回元素序列(sequence)。在Java 8以前,一般方法的返回類型是Collection
,Set
和List
這些接口;還包括Iterable
和數組類型。一般,很容易決定返回哪種類型。規範(norm)是集合接口。若是該方法僅用於啓用for-each循環,或者返回的序列不能實現某些Collection
方法(一般是contains(Object)
),則使用迭代(Iterable
)接口。若是返回的元素是基本類型或有嚴格的性能要求,則使用數組。在Java 8中,將流(Stream)添加到平臺中,這使得爲序列返回方法選擇適當的返回類型的任務變得很是複雜。git
你可能據說過,流如今是返回元素序列的明顯的選擇,可是正如條目 45所討論的,流不會使迭代過期:編寫好的代碼須要明智地結合流和迭代。若是一個API只返回一個流,而且一些用戶想用for-each循環遍歷返回的序列,那麼這些用戶確定會感到不安。這尤爲使人沮喪,由於Stream接口在Iterable接口中包含惟一的抽象方法,Stream的方法規範與Iterable兼容。阻止程序員使用for-each循環在流上迭代的惟一緣由是Stream沒法繼承Iterable。程序員
遺憾的是,這個問題沒有好的解決方法。 乍一看,彷佛能夠將方法引用傳遞給Stream的iterator方法。 結果代碼可能有點嘈雜和不透明,但並不是不合理:github
// Won't compile, due to limitations on Java's type inference for (ProcessHandle ph : ProcessHandle.allProcesses()::iterator) { // Process the process }
不幸的是,若是你試圖編譯這段代碼,會獲得一個錯誤信息:數組
Test.java:6: error: method reference not expected here for (ProcessHandle ph : ProcessHandle.allProcesses()::iterator) {
爲了使代碼編譯,必須將方法引用強制轉換爲適當參數化的Iterable類型:框架
// Hideous workaround to iterate over a stream for (ProcessHandle ph : (Iterable<ProcessHandle>)ProcessHandle.allProcesses()::iterator)
此代碼有效,但在實踐中使用它太嘈雜和不透明。 更好的解決方法是使用適配器方法。 JDK沒有提供這樣的方法,可是使用上面的代碼片斷中使用的相同技術,很容易編寫一個方法。 請注意,在適配器方法中不須要強制轉換,由於Java的類型推斷在此上下文中可以正常工做:ide
// Adapter from Stream<E> to Iterable<E> public static <E> Iterable<E> iterableOf(Stream<E> stream) { return stream::iterator; }
使用此適配器,可使用for-each語句迭代任何流:性能
for (ProcessHandle p : iterableOf(ProcessHandle.allProcesses())) { // Process the process }
注意,條目 34中的Anagrams
程序的流版本使用Files.lines
方法讀取字典,而迭代版本使用了scanner
。Files.lines
方法優於scanner
,scanner
在讀取文件時無聲地吞噬全部異常。理想狀況下,咱們也會在迭代版本中使用Files.lines
。若是API只提供對序列的流訪問,而程序員但願使用for-each語句遍歷序列,那麼他們就要作出這種妥協。學習
相反,若是一個程序員想要使用流管道來處理一個序列,那麼一個只提供Iterable的API會讓他感到不安。JDK一樣沒有提供適配器,可是編寫這個適配器很是簡單:翻譯
// Adapter from Iterable<E> to Stream<E> public static <E> Stream<E> streamOf(Iterable<E> iterable) { return StreamSupport.stream(iterable.spliterator(), false); }
若是你正在編寫一個返回對象序列的方法,而且它只會在流管道中使用,那麼固然能夠自由地返回流。相似地,返回僅用於迭代的序列的方法應該返回一個Iterable
。可是若是你寫一個公共API,它返回一個序列,你應該爲用戶提供哪些想寫流管道,哪些想寫for-each語句,除非你有充分的理由相信大多數用戶想要使用相同的機制。
Collection
接口是Iterable
的子類型,而且具備stream
方法,所以它提供迭代和流訪問。 所以,Collection
或適當的子類型一般是公共序列返回方法的最佳返回類型。 數組還使用Arrays.asList
和Stream.of
方法提供簡單的迭代和流訪問。 若是返回的序列小到足以容易地放入內存中,那麼最好返回一個標準集合實現,例如ArrayList
或HashSet
。 可是不要在內存中存儲大的序列,只是爲了將它做爲集合返回。
若是返回的序列很大但能夠簡潔地表示,請考慮實現一個專用集合。 例如,假設返回給定集合的冪集(power set:就是原集合中全部的子集(包括全集和空集)構成的集族),該集包含其全部子集。 {a,b,c}的冪集爲{{},{a},{b},{c},{a,b},{a,c},{b,c},{a,b , C}}。 若是一個集合具備n個元素,則冪集具備2n個。 所以,你甚至不該考慮將冪集存儲在標準集合實現中。 可是,在AbstractList
的幫助下,很容易爲此實現自定義集合。
訣竅是使用冪集中每一個元素的索引做爲位向量(bit vector),其中索引中的第n位指示源集合中是否存在第n個元素。 本質上,從0到2n-1的二進制數和n個元素集和的冪集之間存在天然映射。 這是代碼:
// 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; } }; } }
請注意,若是輸入集合超過30個元素,則PowerSet.of
方法會引起異常。 這突出了使用Collection
做爲返回類型而不是Stream
或Iterable
的缺點:Collection
有int返回類型的size
的方法,該方法將返回序列的長度限制爲Integer.MAX_VALUE
或231-1。Collection
規範容許size
方法返回231 - 1,若是集合更大,甚至無限,但這不是一個徹底使人滿意的解決方案。
爲了在AbstractCollection
上編寫Collection
實現,除了Iterable
所需的方法以外,只須要實現兩種方法:contains
和size
。 一般,編寫這些方法的有效實現很容易。 若是不可行,多是由於在迭代發生以前未預先肯定序列的內容,返回Stream仍是Iterable的,不管哪一種感受更天然。 若是選擇,可使用兩種不一樣的方法分別返回。
有時,你會僅根據實現的易用性選擇返回類型。例如,假設但願編寫一個方法,該方法返回輸入列表的全部(連續的)子列表。生成這些子列表並將它們放到標準集合中只須要三行代碼,可是保存這個集合所需的內存是源列表大小的二次方。雖然這沒有指數冪集那麼糟糕,但顯然是不可接受的。實現自定義集合(就像咱們對冪集所作的那樣)會很乏味,由於JDK缺乏一個框架Iterator實現來幫助咱們。
然而,實現輸入列表的全部子列表的流是直截了當的,儘管它確實須要一點的洞察力(insight)。 讓咱們調用一個子列表,該子列表包含列表的第一個元素和列表的前綴。 例如,(a,b,c)的前綴是(a),(a,b)和(a,b,c)。 相似地,讓咱們調用包含後綴的最後一個元素的子列表,所以(a,b,c)的後綴是(a,b,c),(b,c)和(c)。 洞察力是列表的子列表只是前綴的後綴(或相同的後綴的前綴)和空列表。 這一觀察直接展示了一個清晰,合理簡潔的實現:
// 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())); } }
請注意,Stream.concat
方法用於將空列表添加到返回的流中。 還有,flatMap
方法(條目 45)用於生成由全部前綴的全部後綴組成的單個流。 最後,經過映射IntStream.range
和IntStream.rangeClosed
返回的連續int值流來生成前綴和後綴。這個習慣用法,粗略地說,流等價於整數索引上的標準for循環。所以,咱們的子列表實現似於明顯的嵌套for循環:
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循環直接轉換爲流。結果比咱們之前的實現更簡潔,但可能可讀性稍差。它相似於條目 45中的笛卡爾積的使用流的代碼:
// 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循環同樣,此代碼不會包換空列表。 爲了解決這個問題,可使用concat
方法,就像咱們在以前版本中所作的那樣,或者在rangeClosed
調用中用(int) Math.signum(start)
替換1。
這兩種子列表的流實現均可以,但都須要一些用戶使用流-迭代適配器( Stream-to-Iterable adapte),或者在更天然的地方使用流。流-迭代適配器不只打亂了客戶端代碼,並且在個人機器上使循環速度下降了2.3倍。一個專門構建的Collection實現(此處未顯示)要冗長,但運行速度大約是個人機器上基於流的實現的1.4倍。
總之,在編寫返回元素序列的方法時,請記住,某些用戶可能但願將它們做爲流處理,而其餘用戶可能但願迭代方式來處理它們。 儘可能適應兩個羣體。 若是返回集合是可行的,請執行此操做。 若是已經擁有集合中的元素,或者序列中的元素數量足夠小,能夠建立一個新的元素,那麼返回一個標準集合,好比ArrayList。 不然,請考慮實現自定義集合,就像咱們爲冪集程序裏所作的那樣。 若是返回集合是不可行的,則返回流或可迭代的,不管哪一個看起來更天然。 若是在未來的Java版本中,Stream
接口聲明被修改成繼承Iterable
,那麼應該隨意返回流,由於它們將容許流和迭代處理。