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 文档。