整理 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 是最通用的回傳型別,能滿足最多用戶端需求。