1. 介紹
先從生活化的例子開始。想像你正在籌辦一場派對並製作賓客名單。你發出邀請後才發現,同一個人被列了兩次(甚至三次——他真的很愛派對!)。如果你使用的是一般的清單(List),這種重複很容易出現。但如果有個集合,會自動不允許將同一位賓客加入兩次——生活就簡單多了。
這時就輪到集合 Set 登場了——「集合」。
Set 是一種只儲存唯一元素的集合。若你嘗試加入已存在的元素,它根本不會被加入(也不會有人介意)。
Set 介面:基本特性
在 Java 中,Set 是一個介面,定義了「無重複」集合的行為。它繼承自 Collection 介面,因此支援新增(add)、刪除(remove)、檢查是否存在(contains)以及遍歷等操作。
關鍵特點:
- Set 中不會有兩個相同的元素。
- 元素的儲存順序可能是任意的——這取決於具體實作。
- 沒有索引:無法像在清單中那樣透過序號存取元素。
宣告語法
Set<String> guests = new HashSet<>();
2. HashSet:快速、簡單、無順序
HashSet 是 Set 介面最常見的實作。它基於雜湊表(和 HashMap 類似,但沒有「鍵-值」配對,只有唯一的值)。最大優勢是操作的速度快——新增、刪除與查找都很快。
HashSet 如何運作?
想像一個收納箱,你把東西放進去。為了快速判斷裡面是否已經有相似的物品,每件物品都會得到自己的「號碼」——雜湊碼。當你把元素加入 HashSet 時,會先計算它的雜湊碼。如果之前沒遇過,就能順利加入集合;如果雜湊碼已存在,還會再透過 equals() 檢查是否相等。只有在物件真的相同時,新元素才不會被加入。
也就是說,HashSet 會自動確保唯一性:兩個相同的物件不會同時出現在其中。
有個重點:如果你使用自訂類別並想把它們存入 HashSet,就必須覆寫 equals() 與 hashCode()。否則集合可能表現得不可預期——明明看起來相同的物件,卻會被視為不同。
HashSet 的基本方法
Set<String> guests = new HashSet<>();
guests.add("伊萬");
guests.add("瑪麗亞");
guests.add("彼得");
guests.add("伊萬"); // 重複!不會加入。
System.out.println(guests); // [伊萬, 瑪麗亞, 彼得] — 順序可能不固定
guests.remove("彼得"); // 刪除元素
System.out.println(guests.contains("瑪麗亞")); // true
System.out.println(guests.size()); // 2
我們來在程式碼中試試看
假設我們的應用程式想要儲存唯一的任務名稱,避免兩個任務同名:
import java.util.HashSet;
import java.util.Set;
public class UniqueTasksDemo {
public static void main(String[] args) {
Set<String> tasks = new HashSet<>();
tasks.add("做 Java 作業");
tasks.add("摸貓");
tasks.add("做 Java 作業"); // 重複!
System.out.println("任務清單:");
for (String task : tasks) {
System.out.println("- " + task);
}
// 清單中只會有兩個任務,重複的不會加入
}
}
3. TreeSet:順序很重要!
有時我們不只需要唯一性,還需要已排序的元素集。比如,想按字母順序查看賓客姓名,而不是隨機順序。此時就用 TreeSet。
TreeSet 是 Set 的一種實作,會以排序後(遞增)的方式儲存元素。它基於「紅黑樹」結構。
TreeSet 的使用範例
import java.util.Set;
import java.util.TreeSet;
public class SortedGuestsDemo {
public static void main(String[] args) {
Set<String> guests = new TreeSet<>();
guests.add("弗拉基米爾");
guests.add("阿列克謝");
guests.add("葉卡捷琳娜");
guests.add("阿列克謝"); // 重複!
System.out.println("來賓(按字母順序):");
for (String guest : guests) {
System.out.println("- " + guest);
}
// 輸出:
// - 阿列克謝
// - 弗拉基米爾
// - 葉卡捷琳娜
}
}
請注意:如果你加入重複項,它不會出現在集合中。一切如預期!
何時使用 TreeSet?
- 當需要已排序的唯一元素集時。
- 當需要快速查找,但對新增速度不那麼敏感(比 HashSet 稍慢)。
- 如果元素是你的自訂類別,必須是「可比較的」(實作 Comparable),或提供自己的 Comparator。
4. 實用細節
HashSet 與 TreeSet:如何選擇?
| 比較準則 | HashSet | TreeSet |
|---|---|---|
| 儲存順序 | 不保證 | 依遞增排序 |
| 操作速度 | 較快(O(1)) | 較慢(O(log n)) |
| 類型要求 | 任意(只需 equals()/hashCode()) | Comparable 或 Comparator |
| 典型情境 | 需要快速存取唯一元素時 | 需要有序輸出/遍歷時 |
使用 Set 的特性
- 沒有索引。 與 List 不同,Set 沒有 get(int index) 方法。若需要透過索引存取,請用 List。
- 沒有重複。 若你嘗試加入已存在的元素,它不會加入。方法 add 會回傳 false。
- 不保證順序(TreeSet 例外)。 在 HashSet 中,元素順序每次執行都可能不同。若需要維持插入順序,請用 LinkedHashSet。
- Null 值。
- HashSet 允許儲存一個 null 元素。
- TreeSet 在沒有特別的 Comparator 時不允許加入 null,否則會拋出 NullPointerException。
5. Set 的典型任務
從列表移除重複項
假設有一個學生清單,其中有人被記錄了兩次。我們需要只保留唯一的姓名:
import java.util.*;
public class RemoveDuplicatesDemo {
public static void main(String[] args) {
List<String> students = Arrays.asList("安娜", "伊戈爾", "安娜", "瑪麗亞", "伊戈爾", "帕維爾");
Set<String> uniqueStudents = new HashSet<>(students);
System.out.println("唯一的學生: " + uniqueStudents);
// 順序不保證!
}
}
如果需要排序後的結果,請使用 TreeSet:
Set<String> sortedUniqueStudents = new TreeSet<>(students);
System.out.println("唯一的學生(按字母順序): " + sortedUniqueStudents);
檢查唯一性(例如使用者登入名)
Set<String> usedLogins = new HashSet<>();
usedLogins.add("student1");
usedLogins.add("java_lover");
String newLogin = "student1";
if (usedLogins.contains(newLogin)) {
System.out.println("此登入名已被佔用!");
} else {
System.out.println("登入名可用!");
}
遍歷集合元素
可使用 for-each 迴圈進行遍歷:
for (String name : uniqueStudents) {
System.out.println(name);
}
6. 使用 Set 的常見錯誤
錯誤 №1:期望 HashSet 中有特定的元素順序。 許多新手會納悶,為什麼集合中的元素輸出順序「怪怪的」。這很正常——HashSet 不保證順序。若需要維持插入順序,請使用 LinkedHashSet;若需要排序,請用 TreeSet。
錯誤 №2:試圖透過索引存取元素。 有時會寫出像 set.get(0) 之類的程式碼。這是不行的:Set 不支援索引。若需要索引式存取,請用 List。
錯誤 №3:儲存可變物件。 如果你儲存的物件,其參與 equals()/hashCode() 的欄位可能會改變,那麼在那些欄位變動後,該元素可能會對集合來說「找不到」。請讓元素不可變,或不要變更識別相關的欄位。
錯誤 №4:以為重複元素會被加入。 多次加入同一個元素不會讓集合的大小增加——重複會被忽略,方法 add 會回傳 false。
錯誤 №5:使用基本型別。 像 Set<int> 這樣的寫法無法編譯。請使用包裝類別:Set<Integer>、Set<Double> 等。
GO TO FULL VERSION