Effective Java Item 47:回傳元素序列時優先使用 Collection 而非 Stream
整理 Effective Java 書中 Item 47: Prefer Collection to Stream 心得筆記
主旨:回傳元素序列時,應優先使用 Collection
Java 8 推出 streams 之後,我們在設計方法時有了新的回傳型別選擇。不過,若你要回傳一串元素,Collection 或其子型別通常仍是最合適的選擇,因為這樣使用者不論是想用 for-each 迴圈還是用 stream pipeline,都能方便操作。除非真的有特殊理由,否則不要只回傳 Stream。
點出問題
在 Java 8 以前,回傳元素序列的常見選擇有:
Collection,Set,List:最常見也最好用的泛型集合介面Iterable:當不需要支援contains()等 Collection 方法時- 陣列:當元素為原始型別或有極高效能需求時
Java 8 加入 Stream 後,很多人以為回傳 Stream 就是新潮正確的做法。但其實 Stream 並沒有取代 iteration,如果你只回傳 Stream,會讓想用 for-each 的使用者非常困擾,因為 Stream 並沒有繼承 Iterable,無法直接用 for-each。
// 看似可以用 method reference 做到,結果不行
for (ProcessHandle ph : ProcessHandle.allProcesses()::iterator) { } // 編譯失敗
範例:回傳 Stream 的迴圈要寫得很噁心
雖然可以靠 adapter 轉型讓 Stream 能用 for-each,但很不直覺又難讀:
// 噁心的 workaround
for (ProcessHandle ph : (Iterable<ProcessHandle>) ProcessHandle.allProcesses()::iterator) { }
比較好的做法是自己寫個 adapter function:
// 把 Stream 轉成 Iterable
public static <E> Iterable<E> iterableOf(Stream<E> stream) {
return stream::iterator;
}
// 就可以這樣用 for-each 了
for (ProcessHandle p : iterableOf(ProcessHandle.allProcesses())) { }
相反地,如果你 API 回傳的是 Iterable,但使用者想用 stream pipeline,也會卡住。這時候也可以自己寫 adapter:
// 把 Iterable 轉成 Stream
public static <E> Stream<E> streamOf(Iterable<E> iterable) {
return StreamSupport.stream(iterable.spliterator(), false);
}
小結論:用 Collection 最萬用
既然 Collection 本身就繼承 Iterable,又有 .stream() 方法可轉成 Stream,回傳 Collection 幾乎可以滿足所有需求。像這樣:
public List<String> getTopUsers() {
return new ArrayList<>(Set.of("Tom", "Mary", "Jack"));
}
真實世界設計案例
1. 回傳一份排行榜前 10 名使用者清單
假設你要寫一個 API,回傳使用者排行榜前十名。這時候資料量不多,也已經整理好,可以直接用 List 回傳:
public List<String> getTopUsers() {
return List.of("Alice", "Bob", "Charlie", "Diana", "Eve",
"Frank", "Grace", "Heidi", "Ivan", "Judy");
}
使用者可以用 for-each 直接列印,或 .stream() 轉換成 stream 做進一步處理,彈性高又易懂:
for (String user : getTopUsers()) {
System.out.println(user);
}
// 或者用 stream 排除特定名字
getTopUsers().stream()
.filter(u -> !u.equals("Eve"))
.forEach(System.out::println);
2. 資料量很大時,就不該用 Collection
如果你要提供一個方法,列出 1 到 10 億的整數,這時候若用 List 就會爆記憶體,應該用 Stream:
public Stream<Integer> getLargeRange() {
return IntStream.rangeClosed(1, 1_000_000_000).boxed();
}
使用者可以用 .limit() 取前幾筆資料,或是 .filter() 找出特定條件的數字,不會一次塞爆記憶體:
getLargeRange().filter(n -> n % 1_000_000 == 0)
.limit(5)
.forEach(System.out::println);
小結
- 公開 API 若要回傳元素序列,優先選擇 Collection 或其子型別,能同時支援 for-each 與 stream。
- 若有記憶體或效能考量,可考慮:
- 自行實作 Collection
- 或直接回傳 Stream / Iterable,但最好提供雙版本方法
Stream尚未繼承Iterable,但若未來有改,則可以只回傳 Stream,並兼顧兩者需求。
// 建議的 API 設計範例
public Collection<User> getUsers(); // for-each 可用
public Stream<User> streamUsers(); // stream pipeline 用
簡單一句話總結:Collection 是最通用的回傳型別,能滿足最多用戶端需求。