1. 概述
- 如果需要自己手动实现一个Set类,我底层可能会选择List类
- 添加元素前,先通过contains() 方法判断是否已经存在重复的元素
- HashSet,顾名思义,就是基于哈希表的set类
- 根据hashCode() 与 equals() 的联系,我们知道:两个对象的hashCode相同,其不一定等价
- 也就是说,哈希表中同一位置可能存在多个元素:hashCode相同,但不等价
- 瞬间就想起了HashMap中使用拉链法解决哈希冲突,HashSet也可以使用该方法
- HashSet的实现,应该最起码是桶数组 + 链表
1.1 HashSet的特性
Hashset的类注释,提供了以下信息
- HashSet基于哈希表实现了Set接口
- 所谓哈希表,就是HashMap实例。也就说,HashSet是基于HashMap实现的
- HashSet中,元素顺序是无序,甚至可能在一段时间内发生变化(rehash导致)
- 允许有且只有一个
null 值,多个null 值与set的定义矛盾 - HashSet不是线程安全的,多线程访问,可以使用
Collections.synchronizedSet() 方法转为线程安全的set类 - 使用fail-fast迭代器:一旦创建了迭代器,除非使用迭代器的remove方法,其他任何改变HashSet结构的方法都将使得迭代器抛出ConcurrentModificationException异常
- 关于HashSet的性能:
- 在散列均匀的情况下,add、remove、contains和size操作都是
O
(
1
)
O(1)
O(1)的时间复杂度
- 基于迭代器的遍历,与元素个数、容量(桶数组大小)均有关系,不能将初始化容量设置的过大,或将loadFactor设置的过小
总结: - 最重要的信息:HashSet基于哈希表实现了Set接口,确切地说,是基于HashMap实现了Set接口 - 其次,就是null 值、元素无序、非线程安全、fail-fast迭代器、性能等问题
1.2 类图
-
HashSet类的声明如下 public class HashSet<E> extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
-
类图如下图所示
- 直观地看,HashSet继承了
AbstractSet 抽象类,实现了Set 、Cloneable 和Serializable 接口 -
AbstractSet 抽象类:对Set接口的骨架级实现,可以最小化实现Set接口所需的工作量 -
Set 接口:不包含重复元素的集合,所谓重复元素,是指:① 存在元素e1、e2且e1.equals(e2) ,② 至多只有一个null 值 -
Cloneable 接口:说明HashSet支持clone操作 -
Serializable 接口:说明HashSet支持序列化与反序列化
1.3 数据结构
1.4 构造方法
- 构造函数如下:
public HashSet() {
map = new HashMap<>();
}
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
2. 重要方法
2.1 add方法
2.2 contains 方法
2.3 remove 方法
2.4 iterator 方法
3. 总结
3.1 偷懒的HashSet实现
- 可以说,HashSet的实现十分偷懒
- 巧妙地借助HashMap构建了哈希表,元素对应key,value无意义使用类常量
PRESENT - 几乎所有方法的实现,都是基于HashMap中线程的方法,简直不费吹灰之力
参考文档:
3.2 为何需要同时重写equals和hashCode方法?
equals 方法
-
Java的超级父类Object中定义了equals 方法具有:自反性、对称性、传递性、一致性、非空性(任何非null 对象,x.equals(null) 返回false) -
Object类中的equals方法实现十分简单:判断两个对象是否相同 public boolean equals(Object obj) {
return (this == obj);
}
-
并且建议:重写类的equals方法,通常需要重写hashCode 方法,以维护hashCode 方法相等的对象必须具有相等的hash code的约定
hashCode 方法
基于哈希表的类中,为什么同时使用equals 和 hashCode方法?
-
HashMap中判断key相等的代码如下,二者是同时使用的 if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
-
hash code不相等,则两个对象肯定不相等;只有hash code相等了,两个对象才可能相等 -
hashCode方法的开销更小,可以率先判断两个对象不相等的情况 -
当hash code相等时,再继续调用equals方法,用于判断两个对象是否真正相等 -
可以说,hashCode方法保证时效性,equals方法保证可靠性
若只重写equals方法,有什么后果?
- Student类,判断两个学生是否为同一个人:
- 是否为同一个对象?
- 不是同一个对象,其关键信息是否相同?
- 如果我们判断两个学生对象相同,但是其hashCode由于未重写,而返回不同的值
- 不仅违反了相等的对象必须具有相等的hash code的约定
- 在基于哈希表的类中,还会出现严重的bug:
- 已存入的键值对,get时,由于key的hash code发生变化而发现不存在
- 已存入的键值对,put时,由于key的hash code发生变化而成功新增,并非更新oldValue;
总结
- hashCode方法可以快速判断两个对象不相等的情况,然后再使用开销更大的equals方法判断两个对象是否相等
- 这样的实现更加高效、可靠
- 如果只重写equals方法,不仅违反了hashCode方法的有关约定,还会导致基于哈希表的类无法正常运行
|