一、简单介绍
HashSet 是一个没有重复元素的集合,实现了 Set 的接口。与 HashMap 一样,HashSet 也不能保证元素的顺序,也可以插入 null 值。HashSet 是基于 HashMap 实现的,只是我们使用 HashMap 的时候会传入 key 和 value ,而使用 HashSet 只会人工传入 key ,value 是由系统自动附加的,这一点本文中会有所提及。
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
二、源码浅析
1. 成员变量
和看 HashMap 一样,我们先看看 HashSet 的成员变量都有哪些,实际上只有3个 —— UID、HashMap、PRESENT常量。
static final long serialVersionUID = -5024744406713321676L;
private transient HashMap<E,Object> map;
private static final Object PRESENT = new Object();
UID 属性我们不在此阐述,只介绍 map 和 PRESENT。
HashMap 的构造是存在 key = E, value = object 的情况的,是非常常见的 HashMap 的构造方法。随后 final 声明了一个 object 类型的常量 PRESENT ,用来代替人工传入的 value 。
2. 构造方法
HashSet 的构造方法有 5 个。
构建一个新的 HashSet ,其所使用的 HashMap 示例的初始容量为 16 ,负载因子为 0.75 。
public HashSet() {
map = new HashMap<>();
}
构造包含指定集合中元素的新集合。HashMap是使用 0.75 的负载因子和初始容量创建的,且初始容量足以包含指定集合中的元素。
public HashSet(Collection<? extends E> c) {
// 利用 Math.max 函数确保最小创建的初始容量大小为 16
// 使用 c.size 和 0.75f 负载因子大小计算合理的初始容量
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
这里需要复习一下 HashMap 的有关知识,代码中有这么一句话 (int) (c.size()/.75f) + 1 ,倘若集合 c 的元素数量为14。那么新的 HashMap 的容量是 19.666 还是 20 还是 32 呢?答案是 32 ,如果忘记了原因,可以去查看 《简单的理解一下 HashMap 的源码》 第二模块第七节,复习一下 HashMap 的有参构造以及 tableSizeFor()方法 的实现。
构建一个新的 HashSet ;其所使用的 HashMap 示例具有指定的初始容量和指定的负载因子。
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}
本质上还是要去看 HashMap 的构造方法,不在过多阐述。
构建一个新的 HashSet ;其所使用的 HashMap 示例具有指定的初始容量和 0.75 的负载因子。
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
一个比较特别的构造方法。这个构造方法只有 LinkedHashSet 会使用。其所使用的 LinkedHashMap 示例具有指定的初始容量和指定的负载因子。
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
3. 常用方法
比较特殊的方法是 add() 和 remove() ,其他方法也写在下面了,想理解更清楚的话还是再去看一遍 HashMap 的源码更合适。
add()
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
add 方法其实并不难,就是调用了 HashMap 的 put 方法,传入的 key 是我们传入的 key ,value 就是前面提到的常量 PRESENT 。add 方法是一个 boolean 返回值的方法,返回的判断条件是 put 的返回值与 null 做比较。这是一个在 HashMap 的源码中没有注意到的细节,什么时候 put 方法会返回一个 null 呢?我们不难会想起 put 本质上是调用了 putVal ,也就是说 putVal 方法在某个情况下会返回 null 。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)
如上是 putVal 的函数名与参数,返回值为 V 类型,也就是 value 对应的类型。而函数体中与 return 有关的代码有两段:
一个是 return oldValue
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
一个是 return null
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
这样看起来感觉本来很清晰的 HashMap 的 put 过程突然又复杂了起来。不妨看一下源码的作者是怎么解释 put 这个方法的:
从上我们不难理解, put 方法的返回值,应当是传入的key对应的上一个value,如果在 HashMap 中没有传入的这个 key ,那么就会返回 null。
再去看 putValu 源码作者的解释:
还是一样的含义,返回值是旧的数据值,如果没有旧的数据值则返回空。
到这里整理一下 HashMap 的 put 方法或者说 putVal 方法什么时候返回 null ,也就是 HashSet 的 add 方法的判断条件合适成立?
传入的 key 在当前持有的 HashMap 集合里不存在时,put 方法以及 putVal 方法会向 HashMap 插入这个新的 key ,并返回 null 。
再去看 add 方法的判断条件: map.put(e, PRESENT)==null ,是不是就非常的简单明了。add 方法本质上还是对 HashMap 进行了“插入”的操作,只是根据“插入”的返回值的差异,来反馈给我们是否“无重复的插入”成功。“插入”操作总是被执行,只有“插入”不存在的值时才会使得表达式成立,向我们返回 true 的结果。
remove()
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
与 add 方法一样。HashSet 的 remove 方法会直接调用 HashMap 的 remove 方法。在 HashMap 中,remove 的源码与解释如下。
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
包括实际调用的 removeNode 方法也在这里贴出
final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable)
与 HashMap 的 put 方法和 putVal 方法一样,他们都会 return 旧的数据值或者说旧的数据节点node,但是如果不存在的话就会返回 null 。
remove 的本意是删除,判断条件自然是要求数据存在才可以删除:map.remove(o)==PRESENT ,使用 add 方法传入的 value 均为常量 PRESENT,所以此处应当获得返回值也是 PRESENT ,成功获得则删除成功,返回 true 即可。反之就是获得了 null ,判断条件不成立,删除失败,因为目标数据不存在。
iterator()
返回的是 HashMap 的 iterator
public Iterator<E> iterator() {
return map.keySet().iterator();
}
size()
返回的是 HashMap 的 size
public int size() {
return map.size();
}
isEmpty()
调用的是 HashMap 的 isEmpty()
public boolean isEmpty() {
return map.isEmpty();
}
containsKey()
调用的是 HashMap 的 containsKey()
public boolean contains(Object o) {
return map.containsKey(o);
}
clear()
调用的是 HashMap 的 clear()
public void clear() {
map.clear();
}
4. 常见问题解释
4.1 HashSet如何保证元素不重复?
其实还是要回到 HashMap 的 put 函数上。
put 函数的执行流程就是:计算 hash 值,判断桶位,判断 key 的 hash 是否相同进而进行 equals 判断。然后再进行插入操作或者修改value的操作。如果判断结果不相同,则插入新的元素。
HashSet 为 value 统一赋值为常量 PRESENT ,所以所谓的“修改”操作其实也没有发生真正意义的修改。
如果向 HashSet 中添加一个重复的元素,那么就会根据 hash 判断到桶位发现 hash 和 equals 的结果都相同,根据 put 返回的值是“旧的数据值”再和 null 进行判断,就可以得知是不是添加了重复的元素。
在这里有一个小小的问题,HashMap 又是如何判断两个 key 是否相同的呢?希望大家在对自定义的对象类型操作时,不要忘记了重写 hashcode方法 和 equals方法。
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
4.2 HashSet和HashMap的区别
HashSet | HashMap |
---|---|
HashSet实现了Set接口 | HashMap实现了Map接口 |
HashSet实际上只存储了对象 | HashMap存储的是键值对 |
HashSet的添加操作为add() | HashMap的添加操作是put() |
HashSet使用成员对象计算hashcode值 | HashMap重写了hashcode方法,实际值是键值对的hashcode异或的结果 |
基于HashMap实现的HashSet操作要更慢 | HashMap的操作更快 |
在之前的 HashMap 的文章中只提到了 hash 方法的实现,是高低16位异或的结果,极大程度的避免了hash碰撞。这里附以 HashMap 的 hashcode 方法,还请将两个方法区分开来,理解两个方法的实际作用。
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
4.3 其他问题
目前查阅网络论坛,了解到的有关 HashSet 的问题都是围绕着它的成员常量 PRESENT 来考察的,还请各位了解 HashSet 的 add 与 remove 方法的操作,以及理解他们的根本 HashMap 的 put 与 remove 方法的返回值。