HashMap及抽象类,接口和变量
使用
构造方法
基于openJDK 的HashMap构造方法,很多人在使用的时候其实并没有注意到构造方法内的参数的存在(比如本人,平常没有注意到这种小细节,面试被问到的时候非常之难受,决心总结一下);
HashMap有以下四种构造方法,
- 包含指定初始化容量和负载因子数的构造函数(其他的构造函数本质是调用改构造函数)
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0) //初始化值小于0时,报非法数据异常
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY) // 初始化容量大于最大容量值时,初始化容量设置为最大值(默认:1 << 30)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity); // tableSizeFor() 函数将初始化容量转换为超过传入值的最小的2的N次幂。因为HashMap要求容量均为2的次幂,这个方法保证了容量永远是2的次幂
}
- 包含初始化容量的构造函数
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
- 无参构造函数 ,其中的初始容量和负载因子都为默认值,分别为(16和0.75),负载因子及初始容量的概念将在后面详写
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
- 将其他的Map的键值对放入新构造的map中
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false); //这里不涉及构造方法内容,不展开讲
}
实例方法
对于HashMap中的方法,最常用的无非是 put
,remove
,get
等,我们从put
方法开始将,将HashMap及其内部结构进行具体的描述。
对于上图的理解,将在源代码的逐行解释中,变得逐渐清晰
put()
,putVal()
及resize()
方法
每当新加入一个元素的时候,不可避免的有可能使用到上面的几种方法,那么我们就来简单的介绍一下其中的源代码吧。
当尝试用一句话概括这个过程的话,就是插入,检测哈希碰撞,发生哈希碰撞时的应对策略(链表链接,红黑树),以及为了预防哈希碰撞而进行的扩容(resize)
public V put(K key, V value) {
//此处的hash()方法,内部还有可以使得结果更加散列的运算内容,减少哈希碰撞的发生
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
//由于和(length-1)运算,length 绝大多数情况小于2的16次方。
//所以始终是hashcode 的低16位(甚至更低)参与运算。要是高16位也参与运算,会让得到的下标更加散列!!!
//所以这样高16位是用不到的,如何让高16也参与运算呢。
//所以才有hash(Object key)方法。让他的hashCode()和自己的高16位^运算。
//所以(h >>> 16)得到他的高16位与hashCode()进行^运算。
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//一些会被用到的变量,此时的变量都为初始值( null,或者0)
Node<K,V>[] tab; Node<K,V> p; int n, i;
//如果 发现tab 或 table 为null(table是存放KV数据的数组。第一次使用的时候被初始化)
//即 当前桶不存在
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//判断将要置入的tab位置是否为空 为null则可以直接将在此处新建一个Node<K,V>
//((n-1)&hash 本质上是 hash % n ,这是在进行哈希运算)
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null); // 调用class Node<K,V>的构造方法新建一个节点
else { // 如果p不为null 则说明tab该下标位置发生了哈希碰撞,
Node<K,V> e; K k;
//在hash值相同的情况下,key也相同,则说明 就是当前的这个Node 所以直接赋值 e = p
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//如果key不同,仅仅是发生哈希碰撞,则要考虑链表或者放入树。
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//这里的binCount也就是用来统计当前这个链表的长度啊有没有超过树化阈值(TREEIFY_THRESHOLD)
for (int binCount = 0; ; ++binCount) {
//p,e就像一个一个遍历过程一样,p用来跑遍历 e = p.next; 底下有 p = e 即(p = p.next)
//如果遍历到最后了以依然没有,就新加进去好了。
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//如果超过了树化阈值 就把他转换为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//这里是用来判断是不是tab数组中的该点的链表中有这个key了
//如果是,则说明找到了对应的key的位置了,所以break就好了
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//结合上文 表达的是 p = p.next;
p = e;
}
}
//说白了 找到了对应键的node,但它的值已经存在了
//要根据 onlyIfAbsent这个参数的策略 来判断 是改变值还是保留原值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
这个方法在HashMap里是空的 ,这是为了LinkedHashMap(一个继承HashMap的子类)的方法
//不展开讲,大概意思将该点移动到最后
afterNodeAccess(e);
return oldValue;
}
}
//计数,用来记录HashMap已经被修改过的次数
++modCount;
//如果当前的map大小已经超过了阈值(最大容量*负载因子),就应该进行扩容
//如果不进行扩容,在新插入<K,V>时,更容易导致发生哈希碰撞,严重影响效率
if (++size > threshold)
resize();
//同样是LinkedHashMap的方法,删除eldest的元素
afterNodeInsertion(evict);
return null;
}
上面putVal()
方法解释了HashMap的存值,在存值中有*一个方法resize()
,他的作用是重新分配调整map的大小,在初始化时,或者是阈值达到负载因子*tab数组的长度
,都会调用这个方法。
下面我们来详细从源码介绍一下resize()
方法。 值得注意的是resize()
方法,返回值为Node<K,V>
数组。
final Node<K,V>[] resize() {
//table是存放KV数据的数组。第一次使用的时候被初始化
//所以每次更新就将table存为oldTab ,即代表旧表 因为table要装入新表的内容
Node<K,V>[] oldTab = table;
//oldCap和oldThr 记录 之前的容量和阈值
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
//旧容量和旧阈值大于0时,就说明 不是初始化
if (oldCap > 0) {
//如果之前的容量已经达到了最大容量就不能扩容了,只能返回旧表了
if (oldCap >= MAXIMUM_CAPACITY) { //MAXIMUM_CAPACITY = 1 << 30
//阈值调整为 INT_MAX
threshold = Integer.MAX_VALUE;
return oldTab;
}
//如果容量为原来的两倍时,不超过最大容量,同时原来的容量也大于等于默认初始化容量
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//tab容量扩大为原来的两倍的同时,阈值扩容了原来的两倍
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold 初始容量处于阈值
newCap = oldThr;
else { // zero initial threshold signifies using defaults 零初始阈值表示使用默认值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}