一、概述
HashSet是 Java 集合框架中的一个核心类,实现了Set接口,基于哈希表(实际是HashMap)实现。
主要特点:
不允许重复元素
允许 null 元素
不保证插入顺序
线程不安全
初始容量和负载因子可配置
二、内部实现由来及原理
HashSet继承和实现关系
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable一、继承关系分析
1.extends AbstractSet<E>
作用:代码复用和通用实现
// AbstractSet提供的核心实现: // 1. 通用方法实现 public abstract class AbstractSet<E> extends AbstractCollection<E> implements Set<E> { // equals() 方法的通用实现 public boolean equals(Object o) { if (o == this) return true; if (!(o instanceof Set)) return false; Collection<?> c = (Collection<?>) o; if (c.size() != size()) return false; try { // 核心算法:检查是否包含所有元素 return containsAll(c); } catch (ClassCastException | NullPointerException unused) { return false; } } // hashCode() 方法的通用实现 public int hashCode() { int h = 0; Iterator<E> i = iterator(); // 所有元素hashCode之和 while (i.hasNext()) { E obj = i.next(); if (obj != null) h += obj.hashCode(); } return h; } // 批量删除的优化实现 public boolean removeAll(Collection<?> c) { Objects.requireNonNull(c); boolean modified = false; if (size() > c.size()) { // 遍历小集合更高效 for (Iterator<?> i = c.iterator(); i.hasNext(); ) modified |= remove(i.next()); } else { // 遍历自身删除 for (Iterator<E> i = iterator(); i.hasNext(); ) { if (c.contains(i.next())) { i.remove(); modified = true; } } } return modified; } }为什么继承AbstractSet而不是AbstractCollection?
AbstractSet提供了针对Set接口的专门实现特别是优化了
equals()和removeAll()方法确保Set的数学特性(唯一性)得到维护
二、实现接口分析
1.implements Set<E>
作用:声明行为契约
public interface Set<E> extends Collection<E> { // Set接口特有的约束 // 添加元素(不允许重复) boolean add(E e); // 添加集合(去重) boolean addAll(Collection<? extends E> c); // 其他从Collection继承的方法... // Set的数学性质定义 // A ∪ B(并集): addAll() // A ∩ B(交集): retainAll() // A - B(差集): removeAll() }Set接口保证的特性:
唯一性:元素不重复(基于
equals()和hashCode())无顺序:不保证插入顺序(具体实现可能提供)
空值:通常允许一个null元素
2.implements Cloneable
作用:支持对象克隆(浅拷贝)
// HashSet中的clone()实现 public Object clone() { try { HashSet<E> newSet = (HashSet<E>) super.clone(); // 复制HashMap(浅拷贝) newSet.map = (HashMap<E, Object>) map.clone(); return newSet; } catch (CloneNotSupportedException e) { throw new InternalError(e); } } // 使用示例 HashSet<String> original = new HashSet<>(Arrays.asList("A", "B", "C")); HashSet<String> cloned = (HashSet<String>) original.clone(); // 注意:这是浅拷贝! HashSet<Person> persons = new HashSet<>(); persons.add(new Person("Alice")); HashSet<Person> clonedPersons = (HashSet<Person>) persons.clone(); clonedPersons.iterator().next().setName("Bob"); // 会影响原集合中的对象!Cloneable接口的特点:
标记接口(没有方法)
允许
Object.clone()方法被调用默认是浅拷贝(只复制引用,不复制对象)
3.implements Serializable
作用:支持对象序列化
// HashSet的序列化实现 private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException { // 写入默认字段 s.defaultWriteObject(); // 写入容量、负载因子、大小 s.writeInt(map.capacity()); s.writeFloat(map.loadFactor()); s.writeInt(map.size()); // 写入所有元素 for (E e : map.keySet()) s.writeObject(e); } private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { // 读取默认字段 s.defaultReadObject(); // 读取容量、负载因子、大小 int capacity = s.readInt(); float loadFactor = s.readFloat(); int size = s.readInt(); // 重建HashMap map = new HashMap<>(capacity, loadFactor); // 读取所有元素 for (int i = 0; i < size; i++) { @SuppressWarnings("unchecked") E e = (E) s.readObject(); map.put(e, PRESENT); } }Serializable接口的作用:
允许对象转换为字节流
支持网络传输和持久化存储
HashSet自定义了序列化逻辑以优化性能
三、设计模式分析
1.模板方法模式(Template Method)
// AbstractSet中的模板方法模式 public abstract class AbstractSet<E> { // 抽象方法,由子类实现 public abstract Iterator<E> iterator(); public abstract int size(); // 模板方法,使用抽象方法 public boolean equals(Object o) { // 使用iterator()和size()这些抽象方法 if (size() != ((Set<?>) o).size()) return false; return containsAll((Collection<?>) o); // 调用具体实现 } }2.适配器模式(Adapter)
// HashSet通过HashMap实现Set接口,是适配器模式的体现 public class HashSet<E> { // 适配的目标对象 private HashMap<E, Object> map; // 适配方法:将Set操作转换为Map操作 public boolean add(E e) { return map.put(e, PRESENT) == null; // 适配 } }四、各层级的责任划分
| 层级 | 责任 | 示例 |
|---|---|---|
| Object | 基础对象操作 | clone(),finalize() |
| AbstractCollection | 集合通用操作 | toString(),toArray() |
| AbstractSet | Set专用操作 | equals(),removeAll()优化 |
| HashSet | 具体实现 | 基于HashMap的实现 |
| Set接口 | 行为契约 | 唯一性保证 |
| Cloneable | 克隆支持 | 浅拷贝能力 |
| Serializable | 序列化支持 | 网络传输、持久化 |
五、总结
继承AbstractSet的作用:
避免重复代码:通用算法已实现
保证正确性:Set特性的正确实现
提高一致性:所有Set实现行为一致
实现Set接口的作用:
定义契约:明确HashSet应提供的方法
多态支持:可用Set类型引用HashSet
框架集成:与其他集合类协同工作
实现Cloneable的作用:
对象复制:提供浅拷贝能力
原型模式:支持基于原型的对象创建
实现Serializable的作用:
持久化:支持对象存储
传输:支持网络传输
分布式:支持远程方法调用
HashSet底层数据结构
// HashSet 内部实际上使用 HashMap 存储元素 private transient HashMap<E, Object> map; // 虚拟值,所有键共享同一个值 private static final Object PRESENT = new Object();HashSet构造函数
// 默认构造函数:初始容量16,负载因子0.75 HashSet<String> set1 = new HashSet<>(); // 指定初始容量 HashSet<String> set2 = new HashSet<>(20); // 指定初始容量和负载因子 HashSet<String> set3 = new HashSet<>(20, 0.8f); // 从其他集合创建 HashSet<String> set4 = new HashSet<>(list);三、核心方法分析
1. 添加元素 -add()
public boolean add(E e) { // 调用 HashMap 的 put 方法 // 如果键不存在,返回 null;如果已存在,返回旧值 return map.put(e, PRESENT) == null; }2. 删除元素 -remove()
public boolean remove(Object o) { // 调用 HashMap 的 remove 方法 return map.remove(o) == PRESENT; }3. 查询元素 -contains()
public boolean contains(Object o) { // 调用 HashMap 的 containsKey 方法 return map.containsKey(o); }四、性能分析
时间复杂度
| 操作 | 平均情况 | 最坏情况 |
|---|---|---|
| 添加 | O(1) | O(n) |
| 删除 | O(1) | O(n) |
| 查询 | O(1) | O(n) |
注意:最坏情况发生在所有元素哈希冲突时,退化为链表或需要树化的情况。
负载因子和扩容机制
// 默认负载因子:0.75 static final float DEFAULT_LOAD_FACTOR = 0.75f; // 扩容公式:size > capacity * loadFactor // 例如:容量16,负载因子0.75,当元素数>12时触发扩容 // 扩容为原来的2倍:newCapacity = oldCapacity * 2五、重要特性详解
1. 元素唯一性原理
// 示例:验证唯一性 HashSet<String> set = new HashSet<>(); set.add("apple"); set.add("apple"); // 不会添加成功,返回 false // 依赖于 hashCode() 和 equals() 方法 // 1. 先比较 hashCode() // 2. 如果 hashCode 相同,再比较 equals()2. 自定义对象的存储
class Person { private String name; private int age; // 必须正确重写 hashCode() 和 equals() @Override public int hashCode() { return Objects.hash(name, age); } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; Person person = (Person) obj; return age == person.age && Objects.equals(name, person.name); } } // 使用 HashSet<Person> people = new HashSet<>(); people.add(new Person("Alice", 25)); people.add(new Person("Alice", 25)); // 不会重复添加六、遍历方式
HashSet<String> set = new HashSet<>(Arrays.asList("A", "B", "C")); // 1. 迭代器遍历 Iterator<String> iterator = set.iterator(); while (iterator.hasNext()) { System.out.println(iterator.next()); } // 2. for-each 遍历(推荐) for (String item : set) { System.out.println(item); } // 3. Java 8+ Stream API set.stream().forEach(System.out::println); // 4. forEach 方法 set.forEach(System.out::println);七、线程安全与同步
1. 非线程安全的解决方案
// 方法1:使用 Collections.synchronizedSet() Set<String> syncSet = Collections.synchronizedSet(new HashSet<>()); // 方法2:使用 CopyOnWriteArraySet(适合读多写少的场景) Set<String> copyOnWriteSet = new CopyOnWriteArraySet<>(); // 方法3:使用 ConcurrentHashMap.newKeySet()(Java 8+) Set<String> concurrentSet = ConcurrentHashMap.newKeySet();八、与相关集合的对比
| 特性 | HashSet | TreeSet | LinkedHashSet |
|---|---|---|---|
| 底层实现 | HashMap | TreeMap | LinkedHashMap |
| 元素顺序 | 无序 | 自然排序 | 插入顺序 |
| 允许 null | 是 | 否 | 是 |
| 性能 | O(1) | O(log n) | O(1) |
| 线程安全 | 否 | 否 | 否 |
九、最佳实践和注意事项
1. 选择合适的初始容量
// 预估元素数量,避免频繁扩容 int expectedSize = 1000; HashSet<String> set = new HashSet<>((int)(expectedSize / 0.75f) + 1);2. 重写 hashCode() 的原则
// 好的 hashCode 方法应该: // 1. 一致性:同一对象多次调用返回相同值 // 2. 高效性:计算快速 // 3. 离散性:不同对象尽量不同哈希值3. 常见陷阱
// 陷阱1:添加后修改对象 HashSet<Person> set = new HashSet<>(); Person p = new Person("Tom", 20); set.add(p); p.setName("Jerry"); // 修改对象属性 set.contains(p); // 可能返回 false // 陷阱2:并发修改 // 多线程环境下需要使用同步机制十、性能优化建议
预估容量:避免自动扩容开销
调整负载因子:
空间敏感:提高负载因子(如 0.8)
时间敏感:降低负载因子(如 0.6)
优化 hashCode():
避免频繁冲突
计算成本低
批量操作:
// 一次性添加所有元素,减少扩容次数 HashSet<String> set = new HashSet<>(list);
十一、实际应用场景
// 场景1:去重 List<String> listWithDuplicates = Arrays.asList("a", "b", "a", "c"); List<String> uniqueList = new ArrayList<>(new HashSet<>(listWithDuplicates)); // 场景2:集合运算 HashSet<Integer> set1 = new HashSet<>(Arrays.asList(1, 2, 3)); HashSet<Integer> set2 = new HashSet<>(Arrays.asList(3, 4, 5)); // 并集 HashSet<Integer> union = new HashSet<>(set1); union.addAll(set2); // 交集 HashSet<Integer> intersection = new HashSet<>(set1); intersection.retainAll(set2); // 差集 HashSet<Integer> difference = new HashSet<>(set1); difference.removeAll(set2);十二、Java 8+ 增强特性
// 1. removeIf:条件删除 HashSet<Integer> numbers = new HashSet<>(Arrays.asList(1, 2, 3, 4, 5)); numbers.removeIf(n -> n % 2 == 0); // 删除偶数 // 2. spliterator():并行处理支持 Spliterator<String> spliterator = set.spliterator();