HashSet保证元素不重复?
说直接一点,其实HashSet的add() 方法中调用的是HashMap的put() 方法. 我们都知道的是Map的key不允许重复, 这其实就是HashSet能够保证元素不重复的真正原理.
稍微跟入源码观察一下
Class HashSet
/**
先看看add方法的实现
*/
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
/**
有些人会问PRESENT是哪里来的参数
*/
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
解释一下?
没什么好解释的
我们能够看到的是,PRESENT作为一个不可修改的对象, 在put方法中当做value传入这样我们只需要关注的就是 map的key的特性 及map中key不允许重复,并且只能有一个null值存在.
至于为什么map的key不允许重复,我们继续深入hashMap的put() 方法看一下
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//putVal()
//onlyIfAbsent是true的话,不要改变现有的值
//evict为true的话,表处于创建模式
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//如果主干上的table为空,长度为0,调用resize方法,调整table的长度
if ((tab = table) == null || (n = tab.length) == 0)
/* 这里调用resize,其实就是第一次put时,对数组进行初始化。
如果是默认构造方法会执行resize中的这几句话:
newCap = DEFAULT_INITIAL_CAPACITY; 新的容量等于默认值16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
threshold = newThr; 临界值等于16*0.75
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab; 将新的node数组赋值给table,然后return newTab
如果是自定义的构造方法则会执行resize中的:
int oldThr = threshold;
newCap = oldThr; 新的容量等于threshold,这里的threshold都是2的倍数,原因在
于传入的数都经过tableSizeFor方法,返回了一个新值
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
threshold = newThr; 新的临界值等于 (int)(新的容量*负载因子)
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab; return newTab;
*/
n = (tab = resize()).length; //将调用resize后构造的数组的长度赋值给n
if ((p = tab[i = (n - 1) & hash]) == null) //将数组长度与计算得到的hash值比较
tab[i] = newNode(hash, key, value, null);//位置为空,将i位置上赋值一个node对象
else { //位置不为空
Node<K,V> e; K k;
if (p.hash == hash && // 如果这个位置的old节点与new节点的key完全相同
((k = p.key) == key || (key != null && key.equals(k))))
e = p; // 则e=p
else if (p instanceof TreeNode) // 如果p已经是树节点的一个实例,既这里已经是树了
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else { //p与新节点既不完全相同,p也不是treenode的实例
for (int binCount = 0; ; ++binCount) { //一个死循环
if ((e = p.next) == null) { //e=p.next,如果p的next指向为null
p.next = newNode(hash, key, value, null); //指向一个新的节点
if (binCount >= TREEIFY_THRESHOLD - 1) // 如果链表长度大于等于8
treeifyBin(tab, hash); //将链表转为红黑树
break;
}
if (e.hash == hash && //如果遍历过程中链表中的元素与新添加的元素完全相同,则跳出循环
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e; //将p中的next赋值给p,即将链表中的下一个node赋值给p,
//继续循环遍历链表中的元素
}
}
if (e != null) { //这个判断中代码作用为:如果添加的元素产生了hash冲突,那么调用
//put方法时,会将他在链表中他的上一个元素的值返回
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null) //判断条件成立的话,将oldvalue替换
//为newvalue,返回oldvalue;不成立则不替换,然后返回oldvalue
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount; //记录修改次数
if (++size > threshold) //如果元素数量大于临界值,则进行扩容
resize();
afterNodeInsertion(evict);
return null;
}
代码中,展示的是putVal() 方法,但是仔细阅读源码我们不难发现,putVal() 方法很多逻辑都是在定位哈希桶数组的位置,通过indexOf() 获取到key在数组中的位置,并且判断该位置上是否有值,有值则默认相同,然后将value值替换
PS:jdk1.7 和 jdk1.8的HashMap类有了很大的变化,如果想深入了解的话,请走下面传送门
HashMap详解