本文介绍Java中HashSet的底层实现。
首先先看一下HashSet这个类的声明:
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
AbstractSet<E>这个抽象类重写了equals()方法和hashCode()方法,Set<E>接口规定了Set应有的一些方法,Cloneable表示这个类的对象可以调用clone()方法进行克隆,java.io.Serializable接口表示这个类的对象可以被序列化,注意这个类没有实现RandomAccess接口,也就是不可以随机访问。
下面看一下这个类的成员变量和构造函数:
static final long serialVersionUID = -5024744406713321676L;
private transient HashMap<E,Object> map;
private static final Object PRESENT = new Object();
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);
}
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
第一个成员变量是serialVersionUID,一个序列版本号。Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体(类)的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。
第二个成员变量是HashMap<E,Object>类型的map,这也说明HashSet实际上底层是HashMap的key值,这也就是他无序且不可重复的原因,而且HashSet内部的许多方法实际上就是通过HashMap的方法实现的。HashMap的底层实现之前写过,请点击此处。注意这里map是使用transient关键字修饰的,这代表了这个字段是不会参与序列化的。这个类通过readObject方法和writeObject方法实现了自定义的序列化。readObject和writeObject方法是private的,不是Object类中的方法,也不是Serializable接口中的方法(当然这个接口中没有任何的方法),但是ObjectOutputStream会通过反射来调用这两个私有方法来进行序列化和反序列化而不是使用默认的序列化方法。这里将map用关键字transient来修饰就是为了自己实现map的序列化,只序列化map中的key,因为我们对这里map的value不关心。
第三个成员变量是Object类型的常量PRESENT,这是HashMap中key值对应的value。
至于构造函数,可以发现暴露出来的构造函数底层都是通过HashMap()的构造函数实现的。
这里还有一个没有暴露出来的构造函数HashSet(int initialCapacity, float loadFactory, boolean dummy),这里dummy实际上是一个哑元,在函数内没有被使用到,这个参数的目的只不过是为了区分构造函数调用的时候底层是HashMap还是LinkedHashMap。 LinkedHashMap可以简单理解为一个有序的HashMap,在采用拉链法解决了哈希碰撞的同时,额外花费了一个双向链表来存储数据插入顺序,所以可以通过这个双向列表实现其有序性。
再来看一下HashSet的常用方法底层实现:
1、public boolean add(E e)方法:
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
很明显HashSet的add()方法完全就是通过HashMap的put()方法实现的,map.put(e, PRESENT)在e这个key值没有的时候才会返回null,否则会返回e这个key值原来对应的value值,也就是当e这个key在HashMap中存在的时候,HashSet的这个add()方法返回false。
2、public boolean remove(Object o)方法:
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
还是没什么可说的,就是调用HashMap的remove(Object o)方法,如果HashMap的remove方法成功删除了会返回被删除的key值对应的value值,否则返回null。也就是调用HashSet的remove(Object o)方法的时候,如果o在这个集合中会返回true,否则返回false。
总结:
1、HashSet是一个无序的哈希集合,不支持随机访问。
2、HashSet底层实际上是利用HashMap实现的,其中HashMap的key值就是对应的哈希集合的值,所以是无序且不重复的。
3、HashSet由于底层是由HashMap实现的,所以无参构造函数和HashMap的无参构造函数一致,因此HashSet的默认初始容量是16,默认加载因子是0.75。
4、HashSet里的map是通过transient关键字修饰的,HashSet里写了readObject方法和writeObject方法,使得map这个字段可以按照我们制定的方法进行序列化和反序列化,而其他字段使用defaultReadObject方法和defaultWriteObject方法按照默认方法进行序列化和反序列化,这样设计其实很合理,毕竟我们只关心map中的key,不关心map中的value,不需要按照默认序列化方式来序列化所有key和value。