首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 綜合教學

HashSet 核心原理與實戰:去重利器、哈希存儲與自定義對象正確存入指南

     編輯:Java 綜合教學

前言:Set 接口的「不重複」承諾

Set 是無序、不重複集合,最多允許一個 null。主流實現:HashSet (哈希表、O(1) 查詢)、LinkedHashSet (維護插入序)、TreeSet (紅黑樹、有序)。本文聚焦 HashSet:底層原理、四大構造器、核心 API、自定義對象正確存入的關鍵 — 重寫 hashCode/equals。

一、HashSet 核心特性

特性說明
底層結構HashMap,值統一用 PRESENT 佔位
去重依據hashCode() 定桶位 + equals() 精確比較
順序性無序,不保證插入/迭代順序
Null 元素允許一個 null
線程安全非線程安全
擴容機制負載因子 0.75,容量翻倍 (2ⁿ)

二、四大構造器

// 1. 默認:容量 16,負載因子 0.75
new HashSet<>();

// 2. 指定初始容量 (避免頻繁擴容)
new HashSet<>(int initialCapacity);

// 3. 指定容量+負載因子
new HashSet<>(int initialCapacity, float loadFactor);

// 4. 從 Collection 複製 (去重)
new HashSet<>(Collection c);

三、基礎 API 實戰

HashSet set = new HashSet<>();
set.add("張三");
set.add("李四");
set.add("jay");
set.add("jack");
boolean added = set.add("jay"); // false,已存在
System.out.println(set); // [李四, 張三, jay, jack] 順序不固定
System.out.println("大小: " + set.size()); // 4

// 遍歷
for (String s : set) System.out.print(s + " ");

// 判斷包含
System.out.println(set.contains("jay")); // true

// 刪除
set.remove("jack");

四、核心難點:自定義對象為何「去重失效」?

現象

class Student {
    String name; int age;
    public Student(String name, int age) { this.name=name; this.age=age; }
    // 缺少 hashCode/equals 重寫!
}

HashSet stuSet = new HashSet<>();
stuSet.add(new Student("張三", 20)); // true
stuSet.add(new Student("張三", 20)); // true ← 應為 false!
System.out.println(stuSet.size()); // 2 ← 預期 1

根因

  • 默認 Object.hashCode() 基於對象地址,兩個 new Student(...) 地址不同 → hashCode 不同 → 落入不同桶 → equals 根本不調用 → 視為不同元素。
  • 默認 Object.equals() 比較引用地址,同樣失效。

解決:必須同時重寫 hashCode() 和 equals()

class Student {
    String name; int age;
    public Student(String name, int age) { this.name=name; this.age=age; }

    @Override public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return age == student.age && Objects.equals(name, student.name);
    }

    @Override public int hashCode() {
        return Objects.hash(name, age); // JDK 7+ 推薦
    }
}

// 再次測試
HashSet stuSet = new HashSet<>();
System.out.println(stuSet.add(new Student("張三", 20))); // true
System.out.println(stuSet.add(new Student("張三", 20))); // false ✅
System.out.println(stuSet.size()); // 1 ✅

五、hashCode/equals 契約鐵律 (必背)

  1. 一致性:a.equals(b) == true ⇒ a.hashCode() == b.hashCode() (反向不要求)。
  2. 對稱性:a.equals(b) ⇔ b.equals(a)。
  3. 傳遞性:a.equals(b) && b.equals(c) ⇒ a.equals(c)。
  4. 非空性:a.equals(null) == false。
  5. 自反性:a.equals(a) == true。

黃金法則:覆蓋 equals 必須覆蓋 hashCode,且兩者依賴的字段集合完全一致。IDEA Generate → equals() and hashCode() 一鍵生成最靠譜。

六、進階:可變對象存入 Set 的陷阱

Student s = new Student("張三", 20);
set.add(s);
s.setAge(21); // 修改了參與 hashCode/equals 的字段!
System.out.println(set.contains(new Student("張三", 20))); // false ❌
System.out.println(set.contains(s)); // true (同引用)
set.remove(new Student("張三", 20)); // 刪除失敗!

結論:作為 Set 元素 / Map 鍵的對象必須不可變 (或至少在集合存活期間不修改參與比較的字段)。

七、總結

  • HashSet = HashMap 鍵集視角,O(1) 去重查詢。
  • 自定義對象存入 必須重寫 hashCode/equals,字段集合一致。
  • 元素對象 保持不可變,避免哈希桶位遷移導致找不到/刪不掉。
  • 需有序用 LinkedHashSet,需排序用 TreeSet (實現 Comparable/傳 Comparator)。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved