CodeGym /課程 /JAVA 25 SELF /Stream API 的子集合:distinct、limit、skip

Stream API 的子集合:distinct、limit、skip

JAVA 25 SELF
等級 30 , 課堂 2
開放

1. 方法 distinct:移除重複項

在實務中,我們常常不僅要篩選與轉換資料,還需要從集合中只取唯一元素、限制結果大小,或者反過來,跳過前幾個元素。例如:

  • 取得唯一的使用者名稱清單。
  • 只取前 10 筆資料用於頁面顯示。
  • 跳過前 5 個元素(例如在分頁時——「顯示第 2 頁」)。

針對這類需求,Stream API 提供了專用方法:distinctlimitskip

distinct 如何運作?

方法 distinct() 會回傳一個新串流,裡面移除了所有重複元素。重複的判定是透過對應類別的 equalshashCode 方法。

想像你是錢幣收藏家,並希望主收藏中沒有重複品。所有重複的硬幣都將放到交換庫。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 物件預設沒有覆寫 equalshashCode。在這種情況下,distinct 只會對引用相等生效。

解法: 覆寫 equalshashCode,讓唯一性依據 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 能正常運作,對自訂類別務必覆寫 equalshashCode

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. 組合 distinctlimitskip

這些方法可以也應該依據需求進行組合。

範例:自第三個開始取得 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,然後使用 skiplimit,最後是 collect。不過若需求需要,順序可以調整。

7. 使用 distinctlimitskip 時的常見錯誤

錯誤 1:期待 distinct 能按「任意」條件去重。
其實 distinct 是依物件的 equals 方法判定。如果你想按某個欄位(例如只看 email)而非整個物件來決定唯一性——就需要覆寫 equals/hashCode,或使用 Collectors.toMap() 等技巧或額外的過濾。

錯誤 2:操作順序不當。
如果先用 limit 再用 distinct,重複項可能仍然存在——因為你已將串流限制在前 N 個元素,而其中可能含有重複。

錯誤 3:skip 的數量大於元素數量。
如果嘗試跳過的數量超過串流的元素數量,結果會是空清單——不會拋錯,但結果可能讓你驚訝。

錯誤 4:忽略效能考量。
distinctlimitskip 在非常大的串流上可能效率不佳,特別是串流無序或元素很複雜時。對 99% 的日常需求不是問題,但如果你處理的是數百萬筆資料——就需要留意。

錯誤 5:忘了為自訂類別覆寫 equals/hashCode
如果你在處理自訂物件(例如 User),不覆寫這些方法時,distinct 會把物件視為不同,即使它們在邏輯上相同。

1
任務
JAVA 25 SELF, 等級 30, 課堂 2
上鎖
專屬派對的隊列管理 🎉
專屬派對的隊列管理 🎉
1
任務
JAVA 25 SELF, 等級 30, 課堂 2
上鎖
生成前四個獨特銷售指標 💰
生成前四個獨特銷售指標 💰
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION