HashSet
HashSet 在概念上就是 Set,而它被叫作 HashSet 的原因也很直观 --- 它的内部实现依赖于 HashTable (HashMap),或者说它的背后实际上就是一个 HashTable。对 HashTable 我们会有一个基本的判断,只要 hash function 能够均匀地把元素分布到不同的 buckets 中,存取元素的操作的均摊 (amortized) 时空复杂度应该都在 O(1)。接下来,我们把 HashTable (HashMap) 当作是给定的组件,来阅读一下 HashSet 的源码。
注:HashTable 和 HashMap 功能上大致相同,主要区别在于 HashTable 是线程安全的而 HashMap 不是线程安全的。在很早以前只有 HashTable 的时候,许多数据结构都是线程安全的,但在后来迭代的过程中发现很多情况是不需要保证线程安全,而保证线程安全会有额外的性能开销,因此 Java 团队决定新的 API 默认不附加线程安全的特性。本文的 HashSet 背后是 HashMap,因此它也不是线程安全的。
说明
- 本文应用范围 Java8
导读
声明
public class HashSet<E> extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable {}
我们观察到以下几点:
- HashSet 是 generic 类:显然 Set 里面放什么类型的对象是任意指定的,但一个集合里面只能放同一种类型的对象
- HashSet 继承 AbstractSet:这个可参考 AbstractSet 的源码阅读
- HashSet 实现 Set<E> 接口:Set 是 Collection 接口下的一个子接口,它定义了一般 Set 具体实现的契约
- HashSet 实现 Cloneable 接口: Cloneable 是一个标记接口 (Marker Interface) ,标记这个 HashSet 支持 clone
- HashSet 实现 Serializable 接口:Serializable 也是标记接口,标记这个 HashSet 支持序列化 (serialization)和反序列化 (deserialization),具体可以参考这篇文章
Class Variables
static final long serialVersionUID = -5024744406713321676L;
private static final Object PRESENT = new Object();
serialVersionUID 用于序列化过程中核验对象的版本,具体请参考声明中提到的文章。只要不同版本 JDK 的 HashSet 实现没有较大的改动,这个 serialVersionUID 不会发生改变。
由于 HashSet 的背后是 HashMap。Map 由键值对组成,我们用 key 代表 Set 中的元素,那么有 key 就必须要有 value,如果我们用 null 作 value,就无法判断 value 对应的 key 是否存在,因此需要一个 NonNull 的值来作 Map 中已经存在的 key 的 value,于是就有了 PRESENT。PRESENT 是一个空的 Object 对象,HashSet 中所有的元素在背后的 HashMap 中的 value 都是 PRESENT。PRESENT 是 Class Variable ,因此空间的浪费在最大程度上被减小。值得一提的是,PRESENT 的本意就是存在的意思。
Instance Variables
private transient HashMap<E, Object> map;
这里除了 transient 其它无需说明。transient 告诉 Java 的序列化 API 这个字段不需要被序列化,在这里实际上是因为默认的 serializer 在性能或者安全性等方面不满足需求,因此 JDK 的作者希望自己来实现 map 的序列化。综上,这里的 transient 是在宣告 “序列化的时候别动它,放着我来!”
Constructors
/* 1 */
public HashSet() {
map = new HashMap<>();
}
/* 2 */
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
/* 3 */
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}
/* 4 */
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
/* 5 */
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
- 在不知道集合中元素的总量时最常用的 constructor,内部的 HashMap 使用默认的 initialCapacity - 16 与 loadFactor - 0.75。
- Collection 的所有实现必须有的 constructor,用作不同 collection 之间的转化。因为知道集合中的元素总量,因此使用与总量相匹配的 initialCapacity,避免重复再分配。
- 自定义内部 HashMap 的两个参数,在明确知道其含义的情况下才使用。
- 自定义 initialCapacity,使用场景见 2
Instance Methods
以下几个 instance methods 复用了 HashMap 中的实现,减少重复代码,提高 JDK code base 的可维护性
iterator
public Iterator<E> iterator() {
return map.keySet().iterator();
}
map.keySet() 返回 HashMap 中定义的 KeySet 类,后者实现了 Set 接口,可以理解成最简版的 Set 实现。
size
public int size() { return map.size(); }
isEmpty
public boolean isEmpty() { return map.isEmpty(); }
contains
public boolean contains(Object o) { return map.containsKey(o); }
add
public boolean add(E e) { return map.put(e, PRESENT) == null; }
如果 map 中已经含有 key e, 则 put 返回 e 之前绑定的值;如果 map 中尚未有 key e,则 put 返回 null。因此如果 HashSet 已经含有元素 e,返回 false;如果 HashSet 尚未含有元素 e,返回 true。
remove
public boolean remove(Object o) { return map.remove(o) == PRESENT; }
注意这里的输入是 Object 类型,map.remove 的返回值与 map.put 类似。
clear
public void clear() { map.clear(); }
clone
public Object clone() {
try {
HashSet<E> newSet = (HashSet<E>) super.clone();
newSet.map = (hashMap<E, Object>) map.clone();
return newSet;
} catch (CloneNotSupportedException e) {
throw new InternalError(e);
}
}
super.clone 是 shallow clone,因此在执行 try block 内第一行代码后,newSet.map 指向 map 本身,这并不是使用者想要的,因此需要第二行代码来 shallow clone map。
writeObject
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);
}
}
s.defaultWriteObject() 执行默认的序列化程序,由于 map 被声明为 transient,因此 map 需要手动序列化,这里手动序列化了 map 的 capacity、loadFactor、size、Set 等信息
readObject
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
int capacity = s.readInt();
if (capacity < 0) {
throw new InvalidObjectException("Illegal capacity: " +
capacity);
}
float loadFactor = s.readFloat();
if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
throw new InvalidObjectException("Illegal load factor: " +
loadFactor);
}
int size = s.readInt();
if (size < 0) {
throw new InvalidObjectException("Illegal size: " +
size);
}
capacity = (int) Math.min(size * Math.min(1 / loadFactor, 4.0f),
HashMap.MAXIMUM_CAPACITY);
SharedSecrets.getJavaOISAccess()
.checkArray(s, Map.Entry[].class, HashMap.tableSizeFor(capacity));
map = (((HashSet<?>)this) instanceof LinkedHashSet ?
new LinkedHashMap<E,Object>(capacity, loadFactor) :
new HashMap<E,Object>(capacity, loadFactor));
for (int i=0; i<size; i++) {
@SuppressWarnings("unchecked")
E e = (E) s.readObject();
map.put(e, PRESENT);
}
}
基本上很清晰,依次把 map 的信息取出来,然后重新初始化 HashMap。 这里 SharedSecrets 一句没有理解,待日后补充。