Java集合框架(二):Set接口与哈希表原理

简介: 本文深入解析Java中Set集合的工作原理及其实现机制,涵盖HashSet、LinkedHashSet和TreeSet三大实现类。从Set接口的特性出发,对比List理解去重机制,并详解哈希表原理、hashCode与equals方法的作用。进一步剖析HashSet的底层HashMap实现、LinkedHashSet的双向链表维护顺序特性,以及TreeSet基于红黑树的排序功能。文章还包含性能对比、自定义对象去重、集合运算实战和线程安全方案,帮助读者全面掌握Set的应用与选择策略。

💡 摘要:你是否曾疑惑为什么Set能自动去重?是否对HashSet的快速查找感到惊奇?是否被HashMap和HashSet的关系搞糊涂?

别担心,Set作为处理唯一性集合的核心接口,其背后的哈希表原理是Java集合框架的精华所在。

本文将带你从Set接口的特性讲起,通过对比List理解Set的去重机制。然后深入哈希表的工作原理,揭开hashCode()和equals()的神秘面纱。

接着详细剖析HashSetLinkedHashSetTreeSet三大实现类的内部机制和适用场景。从哈希冲突解决到红黑树优化,从迭代顺序到性能对比,让你彻底掌握Set集合的精髓。文末附实战案例和面试高频问题,助你在实际开发中游刃有余。

一、Set接口:唯一性的守护者

1. Set的核心特性

与List的对比

特性 List Set
顺序性 插入顺序保存 通常不保证顺序(除LinkedHashSet、TreeSet)
唯一性 允许重复元素 不允许重复元素
null元素 允许多个null 最多允许一个null
实现基础 数组、链表 哈希表、树

Set接口的核心方法

java

// 基本操作

boolean add(E e);           // 添加元素,重复返回false

boolean remove(Object o);   // 删除元素

boolean contains(Object o); // 是否包含元素

void clear();               // 清空集合

int size();                 // 元素数量


// 集合操作

boolean addAll(Collection<? extends E> c); // 并集

boolean retainAll(Collection<?> c);        // 交集  

boolean removeAll(Collection<?> c);        // 差集

2. 去重机制:hashCode()与equals()

Set去重的核心原理

java

// 添加元素时的判断逻辑

public boolean add(E e) {

   return map.put(e, PRESENT) == null; // HashSet底层用HashMap实现

}


// HashMap的put方法核心逻辑

if (e.hash == hash &&

   ((k = e.key) == key || (key != null && key.equals(k)))) {

   // 键已存在,不添加

   return false;

}

二、哈希表原理:快速查找的魔法

1. 哈希表基本概念

哈希函数:将任意大小的数据映射到固定大小值的函数

java

// String类的hashCode实现

public int hashCode() {

   int h = hash;

   if (h == 0 && value.length > 0) {

       char val[] = value;

       for (int i = 0; i < value.length; i++) {

           h = 31 * h + val[i]; // 多项式哈希函数

       }

       hash = h;

   }

   return h;

}

哈希表结构

text

数组 + 链表/红黑树

┌───┐   ┌───┬───┐   ┌───┬───┐

│ 0 │ → │ A │ → │    │   │   │

├───┤   └───┴───┘   └───┴───┘

│ 1 │ → │ B │ → │    │ → │ C  │

├───┤   └───┴───┘   └───┴───┘

│ 2 │ → │ D │ → │    │   │   │

├───┤   └───┴───┘   └───┴───┘

│ 3 │ → null

└───┘

2. 哈希冲突解决

链表法(Separate Chaining)

java

// JDK 1.8之前的实现:数组+链表

if (binCount >= TREEIFY_THRESHOLD - 1) // 链表长度阈值

   treeifyBin(tab, hash);             // 转换为红黑树

开放地址法:Hashtable使用,但HashMap采用链表法

3. HashMap的优化机制

树化优化:当链表长度超过8时,转换为红黑树

java

static final int TREEIFY_THRESHOLD = 8; // 树化阈值

static final int UNTREEIFY_THRESHOLD = 6; // 链化阈值


// 红黑树节点定义

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {

   TreeNode<K,V> parent;  // 父节点

   TreeNode<K,V> left;    // 左孩子

   TreeNode<K,V> right;   // 右孩子

   TreeNode<K,V> prev;    // 前驱节点

   boolean red;           // 颜色标志

}

三、HashSet:基于HashMap的Set实现

1. 内部实现机制

HashSet的本质:基于HashMap的包装类

java

public class HashSet<E> extends AbstractSet<E>

   implements Set<E>, Cloneable, java.io.Serializable {

   

   private transient HashMap<E, Object> map; // 底层HashMap

   

   // 虚拟值,用于HashMap的value

   private static final Object PRESENT = new Object();

   

   public HashSet() {

       map = new HashMap<>(); // 默认初始容量16,加载因子0.75

   }

   

   public boolean add(E e) {

       return map.put(e, PRESENT) == null; // 利用HashMap的key去重

   }

}

2. 构造方法与参数

java

// 1. 默认构造:初始容量16,加载因子0.75

Set<String> set1 = new HashSet<>();


// 2. 指定初始容量

Set<String> set2 = new HashSet<>(100); // 初始容量100


// 3. 指定初始容量和加载因子

Set<String> set3 = new HashSet<>(100, 0.8f); // 容量100,加载因子0.8


// 4. 从其他集合创建

Set<String> set4 = new HashSet<>(list); // 包含list中所有不重复元素

3. 性能特点

时间复杂度

  • add():平均O(1),最坏O(log n)(树化时)
  • remove():平均O(1),最坏O(log n)
  • contains():平均O(1),最坏O(log n)

加载因子与扩容

java

// 默认加载因子0.75:当元素数量达到容量*0.75时扩容

Set<String> set = new HashSet<>(16, 0.75f); // 当size=12时扩容


// 扩容过程:创建新数组(2倍大小),重新哈希所有元素

四、LinkedHashSet:保持插入顺序的Set

1. 内部实现机制

基于LinkedHashMap的实现

java

public class LinkedHashSet<E> extends HashSet<E>

   implements Set<E>, Cloneable, java.io.Serializable {

   

   public LinkedHashSet() {

       super(16, 0.75f, true); // 调用HashSet的特定构造方法

   }

}


// HashSet中的特殊构造方法

HashSet(int initialCapacity, float loadFactor, boolean dummy) {

   map = new LinkedHashMap<>(initialCapacity, loadFactor);

}

2. 双向链表维护顺序

LinkedHashMap的内部结构

text

哈希表 + 双向链表

┌───┐   ┌───────────────┐

│ 0 │ → │ Node1 │ → │ Node2 │

├───┤   ├───────────────┤   ┌───────────────┐

│ 1 │ → │ Node3 │ → │ Node4 │ → │ Node5 │

├───┤   └───────────────┘   └───────────────┘

│ 2 │ → null

└───┘

↑           ↓

头节点       尾节点

3. 适用场景

java

// 需要保持插入顺序的去重集合

Set<String> orderedSet = new LinkedHashSet<>();

orderedSet.add("z");

orderedSet.add("a");

orderedSet.add("b");

orderedSet.add("a"); // 重复元素,不会添加


System.out.println(orderedSet); // [z, a, b] 保持插入顺序


// 实现LRU缓存的基础

Set<String> lruSet = new LinkedHashSet<>(16, 0.75f, true) {

   @Override

   protected boolean removeEldestEntry(Map.Entry<String, Object> eldest) {

       return size() > MAX_ENTRIES;

   }

};

五、TreeSet:基于红黑树的排序Set

1. 内部实现机制

基于TreeMap的实现

java

public class TreeSet<E> extends AbstractSet<E>

   implements NavigableSet<E>, Cloneable, java.io.Serializable {

   

   private transient NavigableMap<E, Object> m; // 底层TreeMap

   

   public TreeSet() {

       this(new TreeMap<E, Object>()); // 自然排序

   }

   

   public TreeSet(Comparator<? super E> comparator) {

       this(new TreeMap<>(comparator)); // 定制排序

   }

}

2. 排序方式

自然排序

java

Set<String> naturalSet = new TreeSet<>();

naturalSet.add("orange");

naturalSet.add("apple");

naturalSet.add("banana");


System.out.println(naturalSet); // [apple, banana, orange] 字母顺序

定制排序

java

// 按字符串长度排序

Set<String> lengthSet = new TreeSet<>((s1, s2) -> {

   int lenCompare = Integer.compare(s1.length(), s2.length());

   return lenCompare != 0 ? lenCompare : s1.compareTo(s2);

});


lengthSet.add("apple");

lengthSet.add("banana");

lengthSet.add("cat");

lengthSet.add("dog");


System.out.println(lengthSet); // [cat, dog, apple, banana]

3. 红黑树特性

自平衡二叉搜索树

  • 每个节点是红色或黑色
  • 根节点是黑色
  • 红色节点的子节点必须是黑色
  • 从任一节点到其每个叶子的所有路径包含相同数目的黑色节点

时间复杂度

  • add():O(log n)
  • remove():O(log n)
  • contains():O(log n)
  • first()/last():O(log n)

六、三大Set实现对比

1. 特性对比表

特性 HashSet LinkedHashSet TreeSet
底层实现 HashMap LinkedHashMap TreeMap
排序特性 无顺序 插入顺序 自然排序/定制排序
性能 O(1)平均 O(1)平均 O(log n)
null元素 允许1个 允许1个 不允许(除非Comparator支持)
线程安全
适用场景 快速去重 保持插入顺序的去重 需要排序的去重

2. 性能测试对比

java

public class SetPerformanceTest {

   public static void main(String[] args) {

       final int SIZE = 100000;

       

       // HashSet测试

       Set<Integer> hashSet = new HashSet<>();

       long start1 = System.nanoTime();

       for (int i = 0; i < SIZE; i++) {

           hashSet.add(i);

       }

       long time1 = System.nanoTime() - start1;

       

       // LinkedHashSet测试

       Set<Integer> linkedSet = new LinkedHashSet<>();

       long start2 = System.nanoTime();

       for (int i = 0; i < SIZE; i++) {

           linkedSet.add(i);

       }

       long time2 = System.nanoTime() - start2;

       

       // TreeSet测试

       Set<Integer> treeSet = new TreeSet<>();

       long start3 = System.nanoTime();

       for (int i = 0; i < SIZE; i++) {

           treeSet.add(i);

       }

       long time3 = System.nanoTime() - start3;

       

       System.out.printf("HashSet: %d ns%n", time1);

       System.out.printf("LinkedHashSet: %d ns%n", time2);

       System.out.printf("TreeSet: %d ns%n", time3);

   }

}

典型结果

text

HashSet: 12500000 ns

LinkedHashSet: 14500000 ns  

TreeSet: 28500000 ns

七、实战应用与最佳实践

1. 自定义对象的去重

java

class Student {

   private String id;

   private String name;

   

   public Student(String id, String name) {

       this.id = id;

       this.name = name;

   }

   

   // 必须重写hashCode和equals

   @Override

   public int hashCode() {

       return Objects.hash(id); // 只基于id计算哈希值

   }

   

   @Override

   public boolean equals(Object obj) {

       if (this == obj) return true;

       if (obj == null || getClass() != obj.getClass()) return false;

       Student other = (Student) obj;

       return Objects.equals(id, other.id); // 只比较id

   }

}


// 使用示例

Set<Student> students = new HashSet<>();

students.add(new Student("1001", "Alice"));

students.add(new Student("1001", "Alice")); // 不会添加,id相同

students.add(new Student("1002", "Bob"));

2. 集合运算实战

java

Set<Integer> set1 = new HashSet<>(Arrays.asList(1, 2, 3, 4, 5));

Set<Integer> set2 = new HashSet<>(Arrays.asList(4, 5, 6, 7, 8));


// 并集

Set<Integer> union = new HashSet<>(set1);

union.addAll(set2); // [1, 2, 3, 4, 5, 6, 7, 8]


// 交集

Set<Integer> intersection = new HashSet<>(set1);

intersection.retainAll(set2); // [4, 5]


// 差集

Set<Integer> difference = new HashSet<>(set1);

difference.removeAll(set2); // [1, 2, 3]


// 对称差集

Set<Integer> symmetricDiff = new HashSet<>(union);

symmetricDiff.removeAll(intersection); // [1, 2, 3, 6, 7, 8]

3. 线程安全方案

java

// 方法1:Collections.synchronizedSet

Set<String> syncSet = Collections.synchronizedSet(new HashSet<>());


// 方法2:CopyOnWriteArraySet(读多写少场景)

Set<String> copyOnWriteSet = new CopyOnWriteArraySet<>();


// 方法3:ConcurrentHashMap.keySet()

Set<String> concurrentSet = ConcurrentHashMap.newKeySet();

八、总结:Set选择的艺术

  1. 默认选择HashSet:大多数去重场景,性能最优
  2. 需要保持顺序选LinkedHashSet:缓存、LRU实现
  3. 需要排序选TreeSet:排行榜、范围查询
  4. 自定义对象必须重写hashCode()和equals()
  5. 线程安全用并发集合:CopyOnWriteArraySet或ConcurrentHashMap.keySet()

🚀 理解Set背后的哈希表原理和红黑树机制,是掌握Java集合框架的关键一步。

九、面试高频问题

❓1. HashSet如何保证元素不重复?

:通过HashMap的key去重。添加元素时调用hashCode()确定存储位置,再用equals()比较是否重复。

❓2. hashCode()和equals()的契约是什么?

  • 两个对象equals()为true,则hashCode()必须相同
  • hashCode()相同的对象,equals()不一定为true
  • 重写equals()必须重写hashCode()

❓3. LinkedHashSet如何保持插入顺序?

:底层使用LinkedHashMap,通过维护双向链表来记录插入顺序。

❓4. TreeSet的排序方式有哪些?

  • 自然排序:元素实现Comparable接口
  • 定制排序:传入Comparator比较器

❓5. 哈希冲突的解决方法有哪些?

  • 链表法:HashMap使用,链表过长时树化
  • 开放地址法:Hashtable使用
  • 再哈希法:使用多个哈希函数
  • 公共溢出区法:将冲突元素放在额外区域
相关文章
|
22天前
|
存储 消息中间件 人工智能
Lazada 如何用实时计算 Flink + Hologres 构建实时商品选品平台
本文整理自 Lazada Group EVP 及供应链技术负责人陈立群在 Flink Forward Asia 2025 新加坡实时分析专场的分享。作为东南亚领先的电商平台,Lazada 面临在六国管理数十亿商品 SKU 的挑战。为实现毫秒级数据驱动决策,Lazada 基于阿里云实时计算 Flink 和 Hologres 打造端到端实时商品选品平台,支撑日常运营与大促期间分钟级响应。本文深入解析该平台如何通过流式处理与实时分析技术重构电商数据架构,实现从“事后分析”到“事中调控”的跃迁。
257 55
Lazada 如何用实时计算 Flink + Hologres 构建实时商品选品平台
|
23天前
|
安全 Java API
Java中的Lambda表达式:简洁与功能的结合
Java中的Lambda表达式:简洁与功能的结合
173 91
|
23天前
|
安全 Java
Java中的Switch表达式:更简洁的多路分支
Java中的Switch表达式:更简洁的多路分支
195 91
|
23天前
|
存储 Java API
Java Stream API:现代数据处理之道
Java Stream API:现代数据处理之道
203 92
|
7天前
|
机器学习/深度学习 监控 数据可视化
基于YOLOv8的打架斗殴暴力行为智能识别项目源码(目标检测)
本系统结合 YOLOv8检测模型 与 PyQt5界面工具,不仅提供完整训练流程,还支持自定义数据集训练,帮助用户快速搭建 开箱即用的打架斗殴行为识别系统。
基于YOLOv8的打架斗殴暴力行为智能识别项目源码(目标检测)
|
8天前
|
缓存 Java 开发者
【Spring】原理:Bean的作用域与生命周期
本文将围绕 Spring Bean 的作用域与生命周期展开深度剖析,系统梳理作用域的类型与应用场景、生命周期的关键阶段与扩展点,并结合实际案例揭示其底层实现原理,为开发者提供从理论到实践的完整指导。
|
16天前
|
运维 Kubernetes 开发者
解锁现代开发与部署:Docker入门指南
解锁现代开发与部署:Docker入门指南
146 100
|
16天前
|
前端开发 JavaScript 开发者
React:构建用户界面的JavaScript库
React:构建用户界面的JavaScript库
|
16天前
|
缓存 负载均衡 前端开发
Nginx:高性能的Web服务器与反向代理利器
Nginx:高性能的Web服务器与反向代理利器
184 99
|
16天前
|
JavaScript 前端开发 API
Vue 3:现代前端开发的革新之作
Vue 3:现代前端开发的革新之作