1. 方法 distinct:移除重複項
在實務中,我們常常不僅要篩選與轉換資料,還需要從集合中只取唯一元素、限制結果大小,或者反過來,跳過前幾個元素。例如:
- 取得唯一的使用者名稱清單。
- 只取前 10 筆資料用於頁面顯示。
- 跳過前 5 個元素(例如在分頁時——「顯示第 2 頁」)。
針對這類需求,Stream API 提供了專用方法:distinct、limit、skip。
distinct 如何運作?
方法 distinct() 會回傳一個新串流,裡面移除了所有重複元素。重複的判定是透過對應類別的 equals 與 hashCode 方法。
想像你是錢幣收藏家,並希望主收藏中沒有重複品。所有重複的硬幣都將放到交換庫。distinct() 正好能從主集合中篩掉重複品。
範例:唯一的使用者名稱
List<String> names = List.of(
"Alice", "Bob", "Alice", "Eva", "Bob", "Denis", "Gleb", "Eva"
);
取得唯一名稱的清單:
List<String> uniqueNames = names.stream()
.distinct()
.collect(Collectors.toList());
System.out.println(uniqueNames);
// 輸出: [Alice, Bob, Eva, Denis, Gleb]
範例:唯一的使用者 email
假設我們有一個 User 類別:
public class User {
String name;
String email;
// 建構子、getter、toString() — 方便展示
public User(String name, String email) {
this.name = name;
this.email = email;
}
@Override
public String toString() {
return name + " <" + email + ">";
}
}
含有重複 email 的使用者清單:
List<User> users = List.of(
new User("Alice", "alice@mail.com"),
new User("Bob", "bob@mail.com"),
new User("Eva", "eva@mail.com"),
new User("Alice2", "alice@mail.com"), // email 重複!
new User("Gleb", "gleb@mail.com"),
new User("Eva2", "eva@mail.com") // email 重複!
);
如果直接呼叫 users.stream().distinct(),重複不會被移除,因為 User 物件預設沒有覆寫 equals 與 hashCode。在這種情況下,distinct 只會對引用相等生效。
解法: 覆寫 equals 與 hashCode,讓唯一性依據 email 判定。
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(email, user.email);
}
@Override
public int hashCode() {
return Objects.hash(email);
}
現在:
List<User> uniqueUsers = users.stream()
.distinct()
.collect(Collectors.toList());
uniqueUsers.forEach(System.out::println);
// Alice <alice@mail.com>
// Bob <bob@mail.com>
// Eva <eva@mail.com>
// Gleb <gleb@mail.com>
重要: 如果希望 distinct 能正常運作,對自訂類別務必覆寫 equals 與 hashCode!
2. 方法 limit:限制元素數量
方法 limit(long maxSize) 會回傳一個新串流,包含原始串流最前面的不超過 maxSize 個元素。
類比: 你走進甜點店想從 100 個甜點中只嚐 3 個。呼叫 limit(3) —— 就只會給你前三個,其他的「今天先不試」。
範例:前 3 個名稱
List<String> firstThree = names.stream()
.limit(3)
.collect(Collectors.toList());
System.out.println(firstThree);
// 輸出: [Alice, Bob, Alice]
與我們的應用程式相關的例子:最新使用者前 3 名
List<User> firstUsers = users.stream()
.limit(3)
.collect(Collectors.toList());
firstUsers.forEach(System.out::println);
// Alice <alice@mail.com>
// Bob <bob@mail.com>
// Eva <eva@mail.com>
搭配排序的範例
可以結合排序使用——例如,取出 email 最短的前 2 位使用者:
List<User> top2ShortEmail = users.stream()
.sorted(Comparator.comparingInt(u -> u.email.length()))
.limit(2)
.collect(Collectors.toList());
top2ShortEmail.forEach(System.out::println);
// Bob <bob@mail.com>
// Eva <eva@mail.com>
3. 方法 skip:跳過前面的元素
方法 skip(long n) 會回傳一個新串流,略過原始串流最前面的 n 個元素。
範例:跳過前 2 個名稱
List<String> afterTwo = names.stream()
.skip(2)
.collect(Collectors.toList());
System.out.println(afterTwo);
// 輸出: [Alice, Eva, Bob, Denis, Gleb, Eva]
範例:分頁(pagination)
網站上常見需求是「每頁顯示 3 位使用者」。第二頁需要跳過前 3 個,取接下來的 3 個:
int pageSize = 3;
int pageNumber = 2; // 第二頁
List<User> page = users.stream()
.skip(pageSize * (pageNumber - 1))
.limit(pageSize)
.collect(Collectors.toList());
page.forEach(System.out::println);
// Alice2 <alice@mail.com>
// Gleb <gleb@mail.com>
// Eva2 <eva@mail.com>
4. 組合 distinct、limit、skip
這些方法可以也應該依據需求進行組合。
範例:自第三個開始取得 2 個唯一名稱
List<String> result = names.stream()
.distinct() // 移除重複: [Alice, Bob, Eva, Denis, Gleb]
.skip(2) // 跳過 Alice 與 Bob: [Eva, Denis, Gleb]
.limit(2) // 只取兩個: [Eva, Denis]
.collect(Collectors.toList());
System.out.println(result);
// 輸出: [Eva, Denis]
搭配過濾的範例
例如,想取得包含字母「a」的前 2 個唯一 email:
List<String> emails = users.stream()
.map(user -> user.email)
.filter(email -> email.contains("a"))
.distinct()
.limit(2)
.collect(Collectors.toList());
System.out.println(emails);
// 輸出: [alice@mail.com, eva@mail.com]
5. 練習:實作題
任務 1:取得長度大於 3 的唯一使用者名稱清單
List<String> longUniqueNames = names.stream()
.filter(name -> name.length() > 3)
.distinct()
.collect(Collectors.toList());
System.out.println(longUniqueNames);
// 例如: [Alice, Denis]
任務 2:取得第 3 與第 4 個唯一 email
List<String> thirdAndFourthEmail = users.stream()
.map(user -> user.email)
.distinct()
.skip(2)
.limit(2)
.collect(Collectors.toList());
System.out.println(thirdAndFourthEmail);
// 例如: [eva@mail.com, gleb@mail.com]
任務 3:取得大於 10 的前 5 個唯一數字
List<Integer> numbers = List.of(5, 12, 17, 5, 23, 17, 42, 19, 12, 8);
List<Integer> result = numbers.stream()
.filter(n -> n > 10)
.distinct()
.limit(5)
.collect(Collectors.toList());
System.out.println(result);
// 輸出: [12, 17, 23, 42, 19]
6. 視覺化示意:操作的套用順序
graph TD
A[原始清單] --> B[filter]
B --> C[distinct]
C --> D[skip]
D --> E[limit]
E --> F[collect]
說明:
通常會先套用 filter,接著去重 distinct,然後使用 skip 與 limit,最後是 collect。不過若需求需要,順序可以調整。
7. 使用 distinct、limit、skip 時的常見錯誤
錯誤 1:期待 distinct 能按「任意」條件去重。
其實 distinct 是依物件的 equals 方法判定。如果你想按某個欄位(例如只看 email)而非整個物件來決定唯一性——就需要覆寫 equals/hashCode,或使用 Collectors.toMap() 等技巧或額外的過濾。
錯誤 2:操作順序不當。
如果先用 limit 再用 distinct,重複項可能仍然存在——因為你已將串流限制在前 N 個元素,而其中可能含有重複。
錯誤 3:skip 的數量大於元素數量。
如果嘗試跳過的數量超過串流的元素數量,結果會是空清單——不會拋錯,但結果可能讓你驚訝。
錯誤 4:忽略效能考量。
distinct、limit、skip 在非常大的串流上可能效率不佳,特別是串流無序或元素很複雜時。對 99% 的日常需求不是問題,但如果你處理的是數百萬筆資料——就需要留意。
錯誤 5:忘了為自訂類別覆寫 equals/hashCode。
如果你在處理自訂物件(例如 User),不覆寫這些方法時,distinct 會把物件視為不同,即使它們在邏輯上相同。
GO TO FULL VERSION