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。

47. 優先使用Collection而不是Stream來作為方法的返回類型
許多方法返回元素序列(sequence)。在Java 8之前,通常方法的返回類型是Collection,Set和List這些接口;還包括Iterable和數組類型。通常,很容易決定返回哪一種類型。規范(norm)是集合接口。如果該方法僅用於啟用for-each循環,或者返回的序列不能實現某些Collection方法(通常是contains(Object)),則使用迭代(Iterable)接口。如果返回的元素是基本類型或有嚴格的性能要求,則使用數組。在Java 8中,將流(Stream)添加到平台中,這使得為序列返回方法選擇適當的返回類型的任務變得非常復雜。
你可能聽說過,流現在是返回元素序列的明顯的選擇,但是正如條目 45所討論的,流不會使迭代過時:編寫好的代碼需要明智地結合流和迭代。如果一個API只返回一個流,並且一些用戶想用for-each循環遍歷返回的序列,那么這些用戶肯定會感到不安。這尤其令人沮喪,因為Stream接口在Iterable接口中包含唯一的抽象方法,Stream的方法規范與Iterable兼容。阻止程序員使用for-each循環在流上迭代的唯一原因是Stream無法繼承Iterable。
遺憾的是,這個問題沒有好的解決方法。 乍一看,似乎可以將方法引用傳遞給Stream的iterator方法。 結果代碼可能有點嘈雜和不透明,但並非不合理:
// 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的類型推斷在此上下文中能夠正常工作:
// 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,那么應該隨意返回流,因為它們將允許流和迭代處理。
