「數組/集合去重」是高頻面試題與日常開發需求。許多開發者只知 new HashSet<>(list) 一把梭,卻在自定義對象去重時翻車。本文層層遞進:從基礎 API、原理機制、自定義對象陷阱,到 equals/hashCode 契約核心,構建完整去重知識體系。
// 重寫 ArrayList.add,每次遍歷判斷
List<String> list = new ArrayList<>() {
@Override public boolean add(String e) {
for (String s : this) if (s.equals(e)) return false;
return super.add(e);
}
};
❌ 10 萬數據新增一個需 10 萬次 equals,性能災難。
public static void removeDuplicate(List<Person> list) {
Set<Person> set = new HashSet<>(list); // 利用 Set 無序、唯一特性
list.clear();
list.addAll(set); // 還原到 List(順序會打亂)
}
✅ O(n) 平均時間,但順序不保留。若需保序,用 LinkedHashSet。
測試代碼:
Person[] data = { new Person(0,"a"), new Person(1,"b"), new Person(0,"a") };
List<Person> list = Arrays.asList(data);
removeDuplicate(list); // 期望去重,實際仍有 3 個元素
HashSet.add(e) → HashMap.put(e, PRESENT)。HashMap.put 判斷 Key 重複邏輯:
// 簡化版 HashMap.put 核心邏輯
int hash = secondaryHash(key.hashCode());
int index = hash & (table.length - 1);
for (Entry e = table[index]; e != null; e = e.next) {
if (e.hash == hash && key.equals(e.key)) { // 關鍵:hash相等 且 equals為true
// 視為重複 Key,覆蓋 value
return oldValue;
}
}
// 插入新節點
結論:兩個對象被視為「相同」需要滿足雙重條件:
hashCode() 相等equals() 返回 trueequals 比較引用地址(==)。hashCode 基於對象頭地址生成。new Person(0,"a") 被視為不同。
public class Person {
private int id;
private String name;
// 構造器、getter/setter、toString 省略
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Person)) return false;
Person other = (Person) obj;
return id == other.id && Objects.equals(name, other.name);
}
@Override
public int hashCode() {
return Objects.hash(id, name); // JDK 7+ 推薦,自動處理 null、質數乘法
// 手寫版:return 31 * id + (name == null ? 0 : name.hashCode());
}
}
a.equals(b) 為 true → a.hashCode() == b.hashCode() 必須為 true。| 場景 | 方案 | 關鍵點 |
|---|---|---|
| List 保序去重 | LinkedHashSet / Stream.distinct() | LinkedHashSet 維護插入順序;Stream 需正確 equals/hashCode |
| 按特定字段去重 (如 id) | TreeSet + Comparator / Stream + 自定義 Key | TreeSet 用 compareTo 判斷相等,僅比較 id;或用 Map<Id, Person> 覆蓋 |
| 大數據流式去重 | Bloom Filter / Bitmap / 外部排序 | 內存不足時的概率/精確算法 |
| 並發環境去重 | ConcurrentHashMap.keySet() / CopyOnWriteArrayList + synchronized | 線程安全 Set 視圖 |
// 保序去重
List<Person> distinct = list.stream().distinct().collect(Collectors.toList());
// 按 id 去重(保留首次出現)
List<Person> distinctById = list.stream()
.collect(Collectors.collectingAndThen(
Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(Person::getId))),
ArrayList::new
));
// 或更通用的自定義 Key 去重工具
public static <T, K> List<T> distinctByKey(List<T> list, Function<T, K> keyMapper) {
Map<K, T> map = new LinkedHashMap<>();
for (T t : list) map.putIfAbsent(keyMapper.apply(t), t);
return new ArrayList<>(map.values());
}
new LinkedHashSet<>(list) (保序) 或 new HashSet<>(list) (不保序)。掌握這套機制,去重不再是「調用 API」,而是「設計契約」。