HashSet 是 Java 集合框架中 Set 接口的经典实现类,基于哈希表(Hash Table)存储元素,核心特点是无序、不允许重复元素、允许 null 值,并提供高效的添加、删除、查找操作。它是日常开发中处理“去重”和“快速查询”场景的常用工具。
例如,分别向 Set 和 List 中插入 100 万数据,然后统计各自判断元素是否存在 100 次,输出耗时时间:
// 创建 set 和 list,并初始化数据为 100 万 Set<String> set = new HashSet<>(); List<String> list = new ArrayList<>(); for(int i = 0; i < 1000000; i++) { String s = "ID" + i; set.add(s); list.add(s); } // 分别统计判断某个值是否存在的 100 次的耗时 // Set(HashSet)通过Hash运算定位 long start = System.currentTimeMillis(); for(int i = 0; i < 100; i++) { set.contains("ID5432100"); } System.out.println("set 耗时:" + (System.currentTimeMillis() - start)); // List(ArrayList)遍历查找 long start2 = System.currentTimeMillis(); for(int i = 0; i < 100; i++) { list.contains("ID5432100"); } System.out.println("list 耗时:" + (System.currentTimeMillis() - start2));
运行结果:
set 耗时:1 list 耗时:836
注意,待查找的元素越靠近列表末尾,耗时越长,因为需要遍历的元素更多。
无序性:元素的存储顺序与添加顺序无关,也不保证元素在迭代时的顺序(底层依赖哈希值分配存储位置,哈希值不连续)。注意,JDK 1.8 后,HashSet 在哈希冲突较多时会将链表转为红黑树,但仍不保证顺序。
去重性:不允许存储重复元素,判断 “重复” 的规则是:
先比较两个元素的 哈希值(hashCode() 方法返回值),若哈希值不同,则认为元素不同;
若哈希值相同,再调用 equals() 方法,若返回 true 则认为元素重复,拒绝添加;若返回 false 则视为不同元素(哈希冲突)。
允许 null 值:最多只能存储 1 个 null 值(符合去重规则)。
线程不安全:非线程安全集合,多线程环境下并发修改(如同时 add/remove)可能导致 ConcurrentModificationException(快速失败机制)。
高效性能:理想情况下(哈希分布均匀,无冲突),add()、remove()、contains() 等操作的时间复杂度为 O(1);若哈希冲突严重,性能会退化至 O (n)(链表结构)或 O (log n)(红黑树结构,JDK 1.8+)。
下面是一个 HashSet 的简单示例,演示初始化、添加元素、删除元素、遍历等常见操作:
package com.hxstrive.java_collection.hashSet; import java.util.HashSet; import java.util.Iterator; import java.util.Set; public class HashSetDemo { public static void main(String[] args) { // 1. 初始化 HashSet(无参构造) Set<String> hashSet = new HashSet<>(); // 2. 添加元素(add()方法) // 返回值为boolean:添加成功返回true,重复元素添加失败返回false boolean isAdded1 = hashSet.add("苹果"); boolean isAdded2 = hashSet.add("香蕉"); boolean isAdded3 = hashSet.add("橙子"); boolean isAdded4 = hashSet.add("苹果"); // 添加重复元素 System.out.println("添加结果:" + isAdded1 + "," + isAdded2 + "," + isAdded3 + "," + isAdded4); System.out.println("添加后集合元素:" + hashSet); // 无序,且无重复元素 // 3. 判断元素是否存在(contains()方法) boolean hasBanana = hashSet.contains("香蕉"); boolean hasGrape = hashSet.contains("葡萄"); System.out.println("\n是否包含香蕉:" + hasBanana); System.out.println("是否包含葡萄:" + hasGrape); // 4. 删除元素(remove()方法) // 返回值为boolean:删除成功返回true,元素不存在返回false boolean isRemoved = hashSet.remove("香蕉"); System.out.println("\n删除香蕉的结果:" + isRemoved); System.out.println("删除后集合元素:" + hashSet); // 5. 集合大小(size()方法) System.out.println("\n集合当前大小:" + hashSet.size()); // 6. 遍历 HashSet 的四种方式 // 方式1:增强for循环(最常用) System.out.println("\n增强for循环遍历:"); for (String fruit : hashSet) { System.out.print(fruit + " "); } // 方式2:迭代器(Iterator) System.out.println("\n\n迭代器遍历:"); Iterator<String> iterator = hashSet.iterator(); while (iterator.hasNext()) { String fruit = iterator.next(); System.out.print(fruit + " "); } // 方式3:forEach + lambda表达式(Java 8+) System.out.println("\n\nlambda表达式遍历:"); hashSet.forEach(fruit -> System.out.print(fruit + " ")); // 方式4:流式处理(Java 8+) System.out.println("\n\n流式处理遍历:"); hashSet.stream().forEach(fruit -> System.out.print(fruit + " ")); // 7. 清空集合(clear()方法) hashSet.clear(); System.out.println("\n\n清空集合后是否为空:" + hashSet.isEmpty()); } }
运行结果:
添加结果:true,true,true,false 添加后集合元素:[苹果, 香蕉, 橙子] 是否包含香蕉:true 是否包含葡萄:false 删除香蕉的结果:true 删除后集合元素:[苹果, 橙子] 集合当前大小:2 增强for循环遍历: 苹果 橙子 迭代器遍历: 苹果 橙子 lambda表达式遍历: 苹果 橙子 流式处理遍历: 苹果 橙子 清空集合后是否为空:true
HashSet 本身不直接实现哈希表,而是依赖 HashMap 存储元素—— 将 HashSet 的元素作为 HashMap 的 Key,Value 则使用一个固定的空对象(PRESENT)占位。
源码如下:
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable { @java.io.Serial static final long serialVersionUID = -5024744406713321676L; /** * 底层存储结构:依赖 HashMap 来存储元素 * HashSet 的元素作为 HashMap 的 key,value 固定为 PRESENT 对象 */ private transient HashMap<E,Object> map; /** * 用于填充 HashMap 的 value 的占位符 * 所有元素共享同一个 PRESENT 对象,节省内存 */ private static final Object PRESENT = new Object(); /** * 创建一个空的 HashSet * 底层 HashMap 使用默认初始容量(16)和默认负载因子(0.75) */ public HashSet() { map = new HashMap<>(); } /** * 创建一个包含指定集合中所有元素的 HashSet * 底层 HashMap 的初始容量会根据指定集合的大小计算,确保足够容纳所有元素 * 负载因子使用默认值 0.75 * * @param c 包含要添加到HashSet中的元素的集合 * @throws NullPointerException 如果指定的集合为 null */ public HashSet(Collection<? extends E> c) { // 计算初始容量:取"集合大小/0.75 + 1" 和 16 中的较大值 // 确保 HashMap 有足够容量,避免频繁扩容 map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16)); // 将集合c中的所有元素添加到当前 HashSet addAll(c); } /** * 创建一个空的 HashSet,指定底层 HashMap 的初始容量和负载因子 * * @param initialCapacity 底层 HashMap 的初始容量 * @param loadFactor 底层 HashMap 的负载因子 * @throws IllegalArgumentException 如果初始容量小于0,或负载因子非正数 */ public HashSet(int initialCapacity, float loadFactor) { map = new HashMap<>(initialCapacity, loadFactor); } /** * 创建一个空的 HashSet,指定底层 HashMap 的初始容量 * 负载因子使用默认值 0.75 * * @param initialCapacity 底层 HashMap 的初始容量 * @throws IllegalArgumentException 如果初始容量小于0 */ public HashSet(int initialCapacity) { map = new HashMap<>(initialCapacity); } /** * 包访问权限的构造方法,仅用于 LinkedHashSet(HashSet 的子类) * 底层使用 LinkedHashMap 作为存储结构,保证元素的插入顺序 * dummy 参数无实际意义,仅用于区分其他相同参数类型的构造方法 * * @param initialCapacity 初始容量 * @param loadFactor 负载因子 * @param dummy 忽略此参数(用于方法签名区分) * @throws IllegalArgumentException 如果初始容量小于0,或负载因子非正数 */ HashSet(int initialCapacity, float loadFactor, boolean dummy) { map = new LinkedHashMap<>(initialCapacity, loadFactor); } }
下面继续介绍 HashSet 如何通过 HashMap 来实现一些关键方法的:
将指定的元素添加到集合中(如果该元素尚未存在)。当集合中不存在满足 Objects.equals(e, e2) 的元素 e2 时,才会将元素 e 添加到集合中。如果集合中已包含该元素,则调用此方法后集合保持不变,并返回 false。源码如下:
/** * 将指定的元素添加到集合中(如果该元素尚未存在)。 * * @param e 要添加到集合中的元素 * @return 如果集合中之前不包含指定元素,则返回 true;否则返回 false */ public boolean add(E e) { return map.put(e, PRESENT) == null; }
将待添加元素 e 作为 HashMap 的 key,PRESENT(固定空对象)作为 value。若 key 不存在,添加后返回 null。若 key 已存在,返回该 key 对应的旧 value(此处为 PRESENT)。因此,当返回值为 null 时,表示添加成功(集合中原本无此元素),返回 true。当返回值为 PRESENT 时,表示添加失败(集合中已有此元素),返回 false。
从集合中移除指定的元素(如果该元素存在)。移除集合中满足 Objects.equals(o, e) 的元素 e(如果存在)。如果集合中包含该元素,则返回 true(等价于:调用此方法导致集合发生了改变)。调用返回后,集合中将不再包含该元素。源码如下:
/** * 从集合中移除指定的元素。 * * @param o 要从集合中移除的对象(如果存在) * @return 如果集合中包含指定元素,则返回 true;否则返回 false */ public boolean remove(Object o) { return map.remove(o) == PRESENT; }
利用底层 HashMap 的 remove 方法实现移除,若 key 存在,移除后返回该 key 对应的 value(此处应为PRESENT),若key不存在,返回 null。因此,当返回值等于 PRESENT 时,表示移除成功(集合中原本包含该元素),返回 true。当返回值为 null 时,表示移除失败(集合中原本不包含该元素),返回 false。
判断当前集合是否包含指定元素。更正式地说,当且仅当集合中存在一个元素 e,且满足 Objects.equals(o, e) 时,才返回 true,否则返回 false。源码如下:
/** * 判断当前集合是否包含指定元素。 * * @param o 要测试是否存在于当前集合中的元素 * @return 若集合包含指定元素,则返回 true;否则返回 false; */ public boolean contains(Object o) { return map.containsKey(o); }
复用底层 HashMap 的 containsKey 方法实现元素存在性判断,因为 HashSet 的元素本质是 HashMap 的 Key,所以判断 "集合是否包含 o",等价于判断 "HashMap 的 Key 中是否存在 o"。
返回此集合中的元素数量(即集合的基数)。源码如下:
public int size() { return map.size(); }
直接返回底层 HashMap 的元素数量,由于 HashSet 的元素都存储为 HashMap 的 Key,因此 HashMap 的 size() 就是 HashSet 的元素数量。
判断当前集合是否不包含任何元素(即是否为空集合)。如果集合中没有元素,则返回 true;否则返回false,源码如下:
public boolean isEmpty() { return map.isEmpty(); }
直接复用底层 HashMap 的 isEmpty() 方法,因为 HashSet 的元素存储在 HashMap 的 Key 中,所以当HashMap 为空时,HashSet 也为空。
更多信息请参考 https://docs.oracle.com/javase/8/docs/api/java/util/HashSet.html API 文档。