HashSet源码解析
Java中的HashSet和TreeSet两个类都是在Map的基础上组装而来的。侧重点也在于Set如何利用Map现有的功能实现自身的设计。
1.HashSet整体架构
HashSet是对HashMap进行组装后的产物,源码文件中对该类的描述具体有如下四点,
- 底层实现基于HashMap,所以迭代过程中不能维护插入顺序。
- add、remove、contains、size等方法的耗能不会随着数据的增加而增加。主要与HashMap底层数组结构相关,不管数据量多大,不考虑hash冲突时,时间复杂度都是O(1)。
- 作为共享变量时,线程不安全,如果需要保证线程安全,则序通过Collections.synchronizedSet方法将其转换为线程安全的集合。
- 迭代过程中如果数据结构发生变化,会抛出ConcurrentModificationException异常。
上面这四点中,2、3、4在List和Map中也都提及,故这三条是List、Map和Set的共同特点。
2.HashSet组合HashMap的方法
Java中基于基础类进行创新的方式主要有两种,
- 继承基础类,复写基础类的方法,比如LinkedHashMap对HashMap访问元素过程在调用get方法时的afterNodeAccess方法进行复写,实现LRU。
- 组合基础类,通过调用基础类的方法,来复用基础类的能力。
HashSet选取的就是组合HashMap的方法,这种方式的好处是,
- 继承表示父类和子类是同一个事物,而Set和Map本身想表达的是两个概念,所以使用继承的方式不妥当。且因为Java语法的限制,子类只能有一个父类,后续难以拓展。
- 组合的方式更加灵活,可以任意组合现有的基础类,并在基础类的方法的基础上进行拓展、编排等。而且方法名称可以任意命名,无需与基础类方法名称保持一致。
在平时的开发中,碰到类似问题应尽量多使用组合,少用继承。
组合HashMap就是将HashMap作为自身的一个局部变量,从HashSet的构造方法中就可以看出,
private transient HashMap<E,Object> map;
// Dummy value to associate with an Object in the backing 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);
}
......
所有的初始化方式中都会初始化一个HashMap对象。同时HashSet类始终存在一个Object对象,该对象在调用add方法时会用到,
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
HashMap中的key是唯一的,HashSet借用了HashMap的这一属性。对于value,设置为相同的值即可。
总结
HashSet内部本质就是HashMap,只是对HashMap做了一层外包。利用HashMap已有的功能完成自己的需求。没有使用继承的方式,使得其方法可以自己定义,无需与HashMap保持一致。
如果想依据key的插入顺序进行遍历,使用LinkedHashSet类即可。LinkedHashSet是基于LinkedHashMap实现的。