什么是 HashSet
?
先简单铺个底:HashSet
是 Java 集合框架里的一个类,基于 HashMap
实现,特点是:
- 无序:元素没有固定顺序。
- 唯一:不允许重复元素(通过
hashCode
和equals
判断)。 - 高效:增删查操作平均时间复杂度是 O(1),适合快速查找和去重。
HashSet
的构造方法决定了它的初始状态(容量、加载因子、初始元素),直接影响性能和内存使用。下面我逐一拆解所有构造方法(基于 Java 17)。
HashSet 的构造方法
HashSet
有 4 个构造方法,我按从简单到复杂讲,每个方法都会说明:
- 语法和参数
- 内部实现
- 使用场景
- 注意事项
- 示例代码
1. HashSet()
public HashSet()
- 作用:
创建一个空的HashSet
,使用默认初始容量 16 和默认加载因子 0.75。 - 参数:无。
- 内部实现:
- 创建一个底层
HashMap
实例,容量设为 16,加载因子 0.75。 HashSet
的元素实际存到HashMap
的键(key)里,值是固定的Object
(占位)。- 源码(简化版):
(public HashSet() { map = new HashMap<>(); }
HashMap
默认初始容量 16,加载因子 0.75)
- 创建一个底层
- 加载因子:
- 加载因子 = 元素数量 ÷ 容量。
- 当元素数量超过
容量 × 加载因子
(16 × 0.75 = 12)时,HashSet
扩容(通常容量翻倍到 32)。
- 使用场景:
- 不确定元素数量。
- 小规模数据(几十个元素),默认设置够用。
- 快速上手,省心。
- 注意事项:
- 如果元素很多(比如上千),频繁扩容会降低性能,建议用带初始容量的构造方法。
- 默认容量 16 可能偏小,内存敏感场景要优化。
- 例子:
HashSet<String> set = new HashSet<>(); set.add("炒鸡VIP"); set.add("普通VIP"); set.add("炒鸡VIP"); // 重复元素,不会添加 System.out.println(set); // 输出:[普通VIP, 炒鸡VIP](顺序随机)
2. HashSet(int initialCapacity)
public HashSet(int initialCapacity)
- 作用:
创建空的HashSet
,指定初始容量,默认加载因子 0.75。 - 参数:
initialCapacity
:初始容量(底层HashMap
的桶数),必须是非负数(≥ 0)。- 如果
initialCapacity < 0
,抛IllegalArgumentException
。
- 内部实现:
- 创建
HashMap
,容量设为initialCapacity
(会向上调整为 2 的幂,比如 100 调整为 128)。 - 加载因子仍是 0.75。
- 源码(简化版):
public HashSet(int initialCapacity) { map = new HashMap<>(initialCapacity); }
- 创建
- 使用场景:
- 知道大概元素数量,提前设容量避免扩容。
- 比如要存 100 个元素,设
initialCapacity = 100
,性能更好。
- 注意事项:
- 容量不是严格等于
initialCapacity
,而是 >=initialCapacity
的最小 2 的幂(HashMap
要求)。 - 设太小,频繁扩容浪费性能;设太大,浪费内存。
- 建议容量设为:
预计元素数 ÷ 加载因子 + 1
(比如 100 个元素,设100 / 0.75 ≈ 134
)。
- 容量不是严格等于
- 例子:
HashSet<String> set = new HashSet<>(100); // 初始容量 100(实际 128) set.add("炒鸡VIP"); System.out.println(set.size()); // 输出:1
3. HashSet(int initialCapacity, float loadFactor)
public HashSet(int initialCapacity, float loadFactor)
- 作用:
创建空的HashSet
,指定初始容量和加载因子。 - 参数:
initialCapacity
:初始容量(≥ 0),否则抛IllegalArgumentException
。loadFactor
:加载因子(通常 0.0 到 1.0),决定扩容时机,否则抛IllegalArgumentException
。
- 内部实现:
- 创建
HashMap
,用指定的initialCapacity
和loadFactor
。 - 容量仍调整为 2 的幂。
- 源码(简化版):
public HashSet(int initialCapacity, float loadFactor) { map = new HashMap<>(initialCapacity, loadFactor); }
- 创建
- 加载因子影响:
loadFactor
小(比如 0.5):扩容早,内存用得多,哈希冲突少,查找快。loadFactor
大(比如 0.9):扩容晚,省内存,冲突多,查找稍慢。
- 使用场景:
- 性能敏感场景,需精细调优。
- 比如内存紧张时用高加载因子(0.9),追求速度时用低加载因子(0.5)。
- 注意事项:
loadFactor
太小(接近 0),扩容过于频繁,内存浪费严重。loadFactor
太大(> 1.0),冲突多,性能下降。- 默认 0.75 是内存和性能的平衡点,改动要谨慎。
- 例子:
HashSet<String> set = new HashSet<>(100, 0.9f); // 容量 100,加载因子 0.9 set.add("炒鸡VIP");
4. HashSet(Collection<? extends E> c)
public HashSet(Collection<? extends E> c)
- 作用:
创建HashSet
,并把另一个集合c
的所有元素加进来(自动去重)。 - 参数:
c
:任意实现了Collection
接口的集合(比如List
、Set
、Queue
)。<? extends E>
:表示c
的元素类型必须是E
或其子类(泛型约束)。
- 内部实现:
- 计算初始容量:
Math.max((int) (c.size() / .75f) + 1, 16)
,确保能装下c
的元素。 - 创建
HashMap
,加载因子 0.75。 - 调用
addAll(c)
,把c
的元素逐个加入(重复元素被忽略)。 - 源码(简化版):
public HashSet(Collection<? extends E> c) { map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16)); addAll(c); }
- 计算初始容量:
- 使用场景:
- 从现有集合(比如
ArrayList
)初始化HashSet
。 - 快速去重(比如把
List
转成无重复的Set
)。
- 从现有集合(比如
- 注意事项:
- 如果
c
是null
,抛NullPointerException
。 c
的大小影响初始容量,过大可能浪费内存。- 元素顺序不保留(
HashSet
无序)。
- 如果
- 例子:
List<String> list = Arrays.asList("炒鸡VIP", "普通VIP", "炒鸡VIP"); HashSet<String> set = new HashSet<>(list); // 去重 System.out.println(set); // 输出:[普通VIP, 炒鸡VIP](顺序随机)
关键概念和注意事项
-
底层是
HashMap
:HashSet
的元素存到HashMap
的键,值是个固定对象(PRESENT = new Object()
)。- 构造方法本质是初始化这个
HashMap
。
-
容量和扩容:
- 容量是底层
HashMap
的桶数,总是 2 的幂。 - 扩容触发:元素数 >
容量 × 加载因子
。 - 扩容代价高(重新分配所有元素),所以初始容量设合理很关键。
- 容量是底层
-
性能优化:
- 初始容量:设为
预计元素数 ÷ 加载因子 + 1
。- 比如存 1000 个元素,
1000 / 0.75 ≈ 1334
,设new HashSet<>(1334)
。
- 比如存 1000 个元素,
- 加载因子:默认 0.75 适合大部分场景,改动需测试。
- 避免频繁扩容:扩容涉及重新哈希,耗时。
- 初始容量:设为
-
线程安全:
HashSet
非线程安全。多线程操作要加锁:Set<String> set = Collections.synchronizedSet(new HashSet<>());
- 或者用
ConcurrentHashMap.newKeySet()
(Java 8+):Set<String> set = ConcurrentHashMap.newKeySet();
-
泛型:
- 总是用泛型,比如
HashSet<String>
或HashSet<Car>
,避免类型转换麻烦。 - 比如你之前代码可能用
HashSet<Car>
,Car
有jiFen
和dengJi
字段:HashSet<Car> mry = new HashSet<>();
- 总是用泛型,比如
-
空元素:
HashSet
允许 一个null
元素(因为HashMap
允许一个null
键)。- 添加多个
null
会被去重。
结合实际场景
假设你要用 HashSet
存你之前代码里的 mry
(比如 Car
对象,带 jiFen
和 dengJi
),选择构造方法的思路:
- 不知道元素数量:用
new HashSet<Car>()
,默认 16 容量。 - 预计 1000 个
Car
:用new HashSet<Car>(1334)
(1000 / 0.75 ≈ 1334)。 - 从
List<Car>
初始化:List<Car> carList = Arrays.asList(new Car(1500, ""), new Car(800, "")); HashSet<Car> mry = new HashSet<>(carList);
- 内存敏感:用
new HashSet<Car>(1000, 0.9f)
,减少扩容。
你的遍历代码可以直接用增强 for
:
HashSet<Car> mry = new HashSet<>();
for (Car car : mry) {
if (car.jiFen > 1200) {
car.dengJi = "炒鸡VIP";
}
}
常见问题和解答
-
为啥容量要是 2 的幂?
HashMap
用位运算(hash & (capacity - 1)
)计算桶索引,2 的幂效率高。- 比如你设 100,实际用 128(2^7)。
-
怎么选初始容量?
- 估算元素数,除以加载因子(默认 0.75),向上取整。
- 比如 100 个元素,
100 / 0.75 ≈ 134
,设 134,实际用 256(2^8)。
-
加载因子改了有啥影响?
- 低加载因子(0.5):更少哈希冲突,查询快,内存多。
- 高加载因子(0.9):省内存,冲突多,查询稍慢。
-
构造方法会影响去重吗?
- 不会!去重靠
hashCode
和equals
,跟构造方法无关。 - 确保
Car
类重写hashCode
和equals
,比如:class Car { int jiFen; String dengJi; @Override public boolean equals(Object o) { /* 比较 jiFen 和 dengJi */ } @Override public int hashCode() { /* 用 jiFen 和 dengJi 计算 */ } }
- 不会!去重靠