【JAVA核心知识】9: 从源码看HashMap:一文看懂HashMap

前言

HashMap是基于Hash表的Map接口实现,允许空值和空键,非线程安全的Map。HashMap不保证顺序,不保证顺序不仅仅是指存储顺序可能与插入顺序不同,还包括元素的存储位置可能会随着对实例的使用而变换。

HashMap的继承关系

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable

可以看到HashMap继承了AbstractMap类,并实现了Map,Cloneable,Serializable接口,这说明HashMap是一个标准Map,且允许克隆和序列化。

1 HashMap的参数

1.1 容量

容量是HashMap的存储桶数。基于HashMap的算法,HashMap内部保证容量一定为2的幂次方。
HashMap并未单独维护属性来记录容量,这是因为桶为数组形式,容量可以直接通过数组的length获得。

1.1.1 默认初始容量

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

HashMap的默认初始容量被定为16,也就是说未指定容量的情况,新的HashMap实例在经历容量初始化之后,容量为16。

1.1.2 最大容量

static final int MAXIMUM_CAPACITY = 1 << 30;

HashMap的最大容量为MAXIMUM_CAPACITY ,对于数组而言,下标为int类型,最大长度是Integer.MAX_VALUE【(1 << 31)-1】,而HashMap对容量的要求为容量必须为2的幂次方,而int类型2的幂次方的最大值即为1 << 30.

1.1.3 最小树化容量

static final int MIN_TREEIFY_CAPACITY = 64;

最小树化容量为HashMap进行树化的最小容量,在容量小于MIN_TREEIFY_CAPACITY 时,如果某个节点满足树化条件时,会优先进行扩容而不是进行节点树化。HashMap建议MIN_TREEIFY_CAPACITY 最小为4*树化阈值。
这么做是因为在容量较小时,hash冲突几率比较大,很容易造成数据聚集,也容易造成树化与扩容的冲突(即新增一个元素到X节点,X节点满足树化阈值了,就进行了节点树化,但是新增的元素使得元素数量也到了扩容阈值,随机有进行了扩容,扩容过程可能造成刚树化的节点即进行树退化,容量越小时,这个冲突发生的几率越大)。
另外容量较小时的树化说明数据聚集严重,会影响到Map的性能。而容量较小时,扩容的代价较小,扩容后却可以获得更均匀的分布,获得更好的性能优势。

1.2 装载因子

装载因子表示存储的KV元素量与HashMap当前容量的比例极限,当HashMap的存储的KV元素量,达到装载因子*当前容量时,HashMap就需要进行容量扩容。
HashMap维护了一个loadFactor属性用来保存装载因子,以在扩容时计算下次的扩容阈值:

final float loadFactor;

1.2.1 默认装载因子

static final float DEFAULT_LOAD_FACTOR = 0.75f;

HashMap默认的装载因子是0.75,也就是在默认情况下,HashMap存储的KV元素量达到容量的3/4时,就需要进行扩容操作。

1.3 阈值

1.3.1 扩容阈值

int threshold;

HashMap内部维护了属性threshold用来记录扩容阈值。扩容阈值的值由装载因子*当前容量计算得出,每次扩容之后都会重新维护,当元素数量达到扩容阈值时,HashMap就会进行容量扩容。

1.3.2 树化阈值

随着元素的新增,节点链表元素数量大于等于该值时,节点链表会转换为红黑树,以获得更好的性能。
默认的树化阈值为;

static final int TREEIFY_THRESHOLD = 8;

1.3.3 树退化阈值

扩容时会进行树切割,如果切割后的树元素量小于等于该值时,红黑树就会退化为链表,以获得更好的性能
默认的树退化阈值为:

static final int UNTREEIFY_THRESHOLD = 6;

1.4 大小

transient int size;

HaspMap内部维护了size属性以实时记录map内的KV映射数量。

1.5 modCount

transient int modCount;

HaspMap内部维护了modCount属性以记录map内部产生的结构改变(指元素数量的修改或类似hash的重新计算这样重新调整存储结构的动作)的次数,以在迭代时对数据准确性进行校验,满足fail-fast。

2 HashMap的数据结构

2.1 数组

transient Node<K,V>[] table;

HashMap内部维护了一个元素类型为内部类Node的数组,元素的所处位置根据其hash计算得到,采用拉链法应对hash冲突。

2.2 链表

在节点元素量达到树化阈值之前,使用链表存储多个元素,链表节点类型为内部类Node:

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash; // key的hash计算结果
        final K key; // map的key
        V value;	// map的value
        Node<K,V> next;	// 指向下一个节点
        ... ...
}

2.3 红黑树

在节点元素量达到树化阈值之后,链表结构会改变为红黑树结构,元素节点类型为内部类TreeNode:

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links  父节点
        TreeNode<K,V> left;	// 左节点
        TreeNode<K,V> right; // 右节点
        TreeNode<K,V> prev;    // needed to unlink next upon deletion 记录前一个元素,补充逆向功能,之前是只通过继承有next属性
        boolean red; 	// 是否为红色节点
        ... ...
}

TreeNode继承自LinkedHashMap.Entry:

static class Entry<K,V> extends HashMap.Node<K,V> {
        Entry<K,V> before, after; // 前后节点
        ... ...
}

LinkedHashMap.Entry继承自HashMap.Node,即为2.2 链表的节点类型,所以TreeNode也就可以作为2.1 数组的元素了。

4 HashMap的下标计算算法

在了解HashMap的扩容步骤之前要先了解一下HashMap如何确认一个元素的下标,以及此种方式与容量的二次幂要求所带来的特性。
HashMap通过hash方法获得一个对象的特征值hash

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

可以看到,hash值的获得是通过对象的hashCode的高16位与低16位异或得到,这么做是为了使高位与低位都参与到下标计算中去,更大程度的去避免hash碰撞。如果key为null,则直接返回0,这就使得HashMap允许key为null。
然后通过e.hash & (newCap - 1),即hash值与(容量-1)进行与运算获得下标。

5 容量为什么一定要是2的幂次方

上文中说过HashMap的容量一定是2的幂次方,且每次常规扩容都是进行二倍扩容。这么做是为了保证每次扩容的效率。
结合HashMap确定元素下标的方式,在此种限制下扩容后的数据就会由一个特性:假设原容量为oldCap,新容量为newCap(2*oldCap),那么原来在j处的数据,扩容后要么在j处,要么在j+oldCap处,这是一种镜像扩容的方式,使得扩容对链表(或者红黑树)的伤害较小,若数据完全平均,则只会有1/2的元素被移动位置。
但是若是并不限制容量规则和扩容规则,允许任意扩容,不规则的扩容会使得原来在j处的对象可能出现在任意位置,此种情况下无论数据是否均匀,那么几乎所有的元素都要进行移动,链表(或者红黑树)会被完成拆散。
例证:假设有两个元素,a.hash的后5位为11101b,b.hash的后5位为01101b,原容量为16(10000b),16-1=15(01111)则a原地址为011101b & 01111b = 1101b,即为13,同理b也为13,扩容后容量为32(100000b),此时计算a新地址为,29(13+16),b依然为13。而若是随机扩容,则不会有这种特性。

6 HashMap的源码实现

6.1 构造函数

6.1.1 无参构造

源码:

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

解析:只讲装载因子赋值为默认装载因子DEFAULT_LOAD_FACTOR

6.1.2 指定初始容量

源码:

public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

解析:调用了另外一个指定初始容量与装载因子的构造函数。

6.1.3 指定初始容量与装载因子

源码:

public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)	// 合法性校验
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY) // 初始容量太的话就直接定义为MAXIMUM_CAPACITY
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor)) // 装载因子要大于0且不为空
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;	// 设置装载因子
    this.threshold = tableSizeFor(initialCapacity); // 调用tableSizeFor获得扩容阈值threshold并赋值
}

解析:赋值装载因子与扩容阈值。6.1.3.1 tableSizeFor(int cap) 的作用是获得入参的最小2次幂,因此扩容阈值threshold被赋值为入参初始容量的最小2次幂。1.1 容量篇描述过,容量必须为2的幂次方,1.3.1 扩容阈值篇描述扩容阈值=装载因子*当前容量,但是在这里扩容阈值threshold却被设置为最小二次幂,那么扩容时容量岂不是可能无法满足2的幂次方的要求了?其实不然,针对这种情况,在扩容方法里有特殊处理,这里设置的扩容阈值threshold并不是实际意义上的扩容阈值,而是暂存首次扩容目标容量,以在实际初始化时获得首次初始化的目标容量。具体可查看4.M.1 resize()
6.1.3.1 tableSizeFor(int cap)
源码:

static final int tableSizeFor(int cap) {
       int n = cap - 1;
       n |= n >>> 1;
       n |= n >>> 2;
       n |= n >>> 4;
       n |= n >>> 8;
       n |= n >>> 16;
       return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
   }

解析:通过位运算获得入参的最小二次幂。特殊的如果结果小于0则返回1,结果大于MAXIMUM_CAPACITY则返回MAXIMUM_CAPACITY。
通过无符号的位右移计算最小二次幂。示例: 11(1011b),进过多次右移之后成为15(1111b),然后在加一成为16(10000b),即为大于11的最小二次幂。

6.1.4 带有初始元素集合

源码:

public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

解析:设置默认的装载因子,并调用6.1.4.M.1 final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) 进行元素新增。
6.1.4.M.1 final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict)
源码:

final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    int s = m.size();
    if (s > 0) {	// 如果集合不为空
        if (table == null) { // pre-size	// 未初始化的话
            float ft = ((float)s / loadFactor) + 1.0F; // 获得能符合容量,大小,装载因子三者条件的最小容量值
            int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                     (int)ft : MAXIMUM_CAPACITY); // 最小容量值大于MAXIMUM_CAPACITY的话就直接赋值MAXIMUM_CAPACITY
            if (t > threshold)	// 最小容量值大于threshold的话,这是一定的,未初始化的threshold是0,就取最小容量值的最小二次幂赋值给扩容阈值threshold
                threshold = tableSizeFor(t);
        }
        else if (s > threshold) // 如果已经初始化过,且集合元素数量大于扩容阈值,就先扩容一下
            resize();
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
            K key = e.getKey();
            V value = e.getValue();
            putVal(hash(key), key, value, false, evict);	// 循环进行元素新增
        }
    }
}

解析:
此方法有两处调用,一处是来自6.1.4 带有初始元素集合的构造方法,一处是来自HashMap的putAll方法。
如果实例还未初始化,那么就要计算出要存储m 中的所有元素,需要多大的容量,从而一次性扩容到位,避免重复扩容,此时可以知道m 中的元素数量s,根据1.3.1 扩容阈值可知,s是要小于扩容阈值threshold的,那么容量的最小值就是float ft = ((float)s / loadFactor) + 1.0F,再根据1.1 容量篇描容量必须为2的幂次方,因此就要计算出ft的最小二次幂获得最小容量。 至于为什么将计算出的容量赋值到扩容阈值threshold而不是直接初始化数组,一是因为HashMap采用的是懒加载初始化,二是和6.1.3 指定初始容量与装载因子一样,此处的扩容阈值threshold只起一个暂存首次扩容目标容量的作用,以在实际初始化时获得首次初始化的目标容量。同样可查看4.M.1 resize()
当然如果HashMap已经初始化过了,那么如果s大于扩容阈值threshold,那么就先进行一次扩容,当然如果s过于大,在后续的6.3.M.1 putVal过程中可能会经历第二次,第三次扩容,这里只是知道容量肯定不够,提前进行一次预扩容而已,并不会保证扩容后的容量一定能满足要求.
最后就是循环将m 中的元素通过6.3.M.1 putVal方法载入HashMap。

6.2 HashMap的扩容

HashMap的通过内部default级别的resize()方法进行容量扩容,未初始化容器的实例调用此方法会进行初始化,已初始化容器的实例调用此方法则会固定进行一次二倍容量的扩容:

6.2.1 扩容方法:resize()

源码:

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table; // 记录旧容器
    int oldCap = (oldTab == null) ? 0 : oldTab.length; // 旧容量
    int oldThr = threshold;	// 就扩容阈值
    int newCap, newThr = 0;	// 新容量和新扩容阈值的定义
    if (oldCap > 0) {	// 旧容量大于0,即已经容器已经过初始化了
        if (oldCap >= MAXIMUM_CAPACITY) {	// 旧容量大于等于MAXIMUM_CAPACITY的话,扩容阈值设置为Integer.MAX_VALUE,然后直接返回旧容器,就是不扩容了
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && // 新容量为旧容量的二倍,如果扩容后的新容量小于MAXIMUM_CAPACITY 
                 oldCap >= DEFAULT_INITIAL_CAPACITY) // 并且旧容量大于等于DEFAULT_INITIAL_CAPACITY
            newThr = oldThr << 1; // double threshold	// 此时才将扩容阈值随着容量也乘以2,这是因为其他场景已属于非常规扩容,需要再次进行计算了
    }
    else if (oldThr > 0) // 旧容量不大于0,但是旧扩容阈值有值,这标识着这个实例创建时指定了容量,但是还没初始化,此时就扩容阈值就是实际的扩容容量
        newCap = oldThr;
    else {               // 旧扩容阈值也不大于0的话,说明这个实例没有任何初始容量要求,那么都是默认值了
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {	//新扩容阈值为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;	// 新容器赋值到table
    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);	// 进行树拆分,计算元素下标,要么依然在j处,要么在j+oldCap处,原因详见:5 容量为什么一定要是2的幂次方
                else { // 不是树的话就进行链表拆分
                    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) {	// 为0说明这个元素在原位,即为低位
                            if (loTail == null)
                                loHead = e;	// 记录低位抬头
                            else
                                loTail.next = e;	// 链接到loHead 所在的链表上
                            loTail = e;	// 记录尾部
                        }
                        else {	// 不为为0说明这个元素在j + oldCap,即为高位
                            if (hiTail == null)
                                hiHead = e;	// 记录高位抬头
                            else
                                hiTail.next = e;// 链接到hiTail所在的链表上
                            hiTail = e;	// 记录尾部
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {	// loTail 不为空说明低位有值
                        loTail.next = null;	// 断开loTail.next的链接,因为loTail为尾部,若是loTail.next不为空,那么他会在高位
                        newTab[j] = loHead;	// 赋值到新容器的对应位置
                    }
                    if (hiTail != null) {	// hiTail 不为空说明高位有值
                        hiTail.next = null; // 断开hiTail.next的链接,因为hiTail为尾部,若是hiTail.next不为空,那么他会在低位
                        newTab[j + oldCap] = hiHead;// 赋值到新容器的对应位置
                    }
                }
            }
        }
    }
    return newTab;
}

解析:扩容整体分为两个部分,第一步为确定各项数据参数的部分,第二部分为实际的容量部分。
扩容的参数确定主要确定新容量与新扩容阈值。

if(容器已经初始化) {
	if(原容量已达到MAXIMUM_CAPACITY) {
		不再进行库容
		新扩容阈值定位 Integer.MAX_VALUE
	} else {
		新容量 = 原容量*2
		if(新容量未达到MAXIMUM_CAPACITY且旧容量大于等于DEFAULT_INITIAL_CAPACITY) {
			新扩容阈值 = 旧扩容阈值*2
		}
	} else if(原扩容阈值大于0) {
		// 此种情况就是就是6.1.3 指定初始容量与装载因子以及6.1.4 带有初始元素集合提到的
		// 目标容量会被暂存到扩容阈值字段,以在实际使用时才进行懒初始化
		新容量 = 原扩容阈值。 
	} else {
		容量与扩容阈值均取默认值
	}
}
if(新扩容阈值等于0) {	// 这种就是上述没有设置扩容阈值的场景,扩容阈值要重新计算
新库容阈值 = 新容量*装载因子 // 因为装载因子可以自己设置,扩容阈值不一定小于容量
if ( 如果新容量大于等于MAXIMUM_CAPACITY 或者新扩容阈值大于等于MAXIMUM_CAPACITY ){
	新库容阈值重置为 Integer.MAX_VALUE
}
}

扩容操作根据节点情况分为两种:一种是链表的拆分,逻辑为遍历,将链拆为两个独立链表赋值即可。
一种是红黑树的拆分,这时就会涉及到树退化的问题。
值得注意的是JDK1.8之前的HashMap的扩容阶段可能出现死锁问题,1.8进行了修复。相关信息可见:从源码看jdk1.8之前的HashMap为什么会产生死锁

6.2.2 扩容中红黑树的拆分

源码:

final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
    TreeNode<K,V> b = this;
    // Relink into lo and hi lists, preserving order
    TreeNode<K,V> loHead = null, loTail = null;	// 低位头部和尾部
    TreeNode<K,V> hiHead = null, hiTail = null;	// 高位同步和尾部
    int lc = 0, hc = 0;
    for (TreeNode<K,V> e = b, next; e != null; e = next) {
        next = (TreeNode<K,V>)e.next;
        e.next = null;
        if ((e.hash & bit) == 0) {	// 为0说明在低位
            if ((e.prev = loTail) == null)
                loHead = e;	/// 是第一个元素的话就记录头
            else
                loTail.next = e;	// 链接到loHead 所在的链表上
            loTail = e;		// 记录尾部
            ++lc;	// 计算低位数量
        }
        else {
            if ((e.prev = hiTail) == null)
                hiHead = e; // 是第一个元素的话就记录头
            else
                hiTail.next = e;// 链接到hiHead 所在的链表上
            hiTail = e;	// 记录尾部
            ++hc;	// 计算高位数量
        }
    }
	
    if (loHead != null) { // loHead不为空说明低位有值
        if (lc <= UNTREEIFY_THRESHOLD)	// 如果拆分后的低位数量小于等于树退化阈值
            tab[index] = loHead.untreeify(map);	// 进行树退化
        else {	// 不满足树退化时
            tab[index] = loHead;// 头赋值到容器对应位置	
            if (hiHead != null) // 如果高位有值,说明确实有元素拆出去,此种情况就要对loHead重新树化
                loHead.treeify(tab);
        }
    }
    if (hiHead != null) { // hiHead 不为空说明低位有值
        if (hc <= UNTREEIFY_THRESHOLD)	// 如果拆分后的高位数量小于等于树退化阈值
            tab[index + bit] = hiHead.untreeify(map);	// 进行树退化
        else {	// 不满足树退化时
            tab[index + bit] = hiHead;// 头赋值到容器对应位置	
            if (loHead != null)// 如果低位有值,说明确实有元素拆出去,此种情况就要对hiHead重新树化
                hiHead.treeify(tab);
        }
    }
}

解析:红黑树的拆分首先也是将树中元素分为低位元素和高位元素。然后分别进行判断,拆分后的元素数量小于等于树退化阈值,就将红黑树退化为链表,否则就根据原树是否完整来判断是否重新树化。

6.3 新元素的载入

新元素的载入以常用的put(K key, V value)为例,解析HashMap如何将数据载入容器:
6.3.M put(K key, V value)
源码:

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

解析:调用hash方法(上文4 HashMap的下标计算算法中已解析)获得key的特征值,然后调用了putVal方法,实际上HashMap中大多数的元素新增都是通过putVal方法来完成的。
6.3.M.1 putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict)

源码:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)	// 容器没初始化的话
        n = (tab = resize()).length;	// 初始化容器,并获得容量
    if ((p = tab[i = (n - 1) & hash]) == null)	// 计算出新元素的下标所在位置,然后看那个位置是不是空的,如果是空的,说明是第一个元素
        tab[i] = newNode(hash, key, value, null);	// 第一个元素直接新建一个节点赋值到对应位置就可以了
    else {	// 已有元素的话
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))	// key和首节点相等的话
            e = p;	// 说明是一个覆盖操作,首节点p赋值给承载元素e就可以了
        else if (p instanceof TreeNode)	// 如果是一个树化的节点的话
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);	// 树化赋值
        else {	// 不是树化的那就是链表了
            for (int binCount = 0; ; ++binCount) {	// 循环找
                if ((e = p.next) == null) {	// 如果找到最后还没找到和key相同的已有节点,说明是一个新的节点
                    p.next = newNode(hash, key, value, null);	// 建立一个新节点并链接上
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);	// 如果原有元素数量差一个就满足树化阈值了,那么加上这个刚好就满足了,执行树化
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;	// 和已有节点key相等的,就跳出循环,不找了
                p = e;	// e赋值给p,即向后移动一位
            }
        }
        if (e != null) { // e有值,那就说明是一个覆盖操作了
            V oldValue = e.value;	
            if (!onlyIfAbsent || oldValue == null)
                e.value = value; // 如果不是仅赋值不存在,或是原值为null,就直接改变节点e的value就行了
            afterNodeAccess(e);	// 如果赋值时有冲突的后置操作,开发者可以重写这个方法,然后做自己想要完成的事情,比如打个日志什么的
            return oldValue;
        }
    }
    ++modCount;	// 修正modCount
    if (++size > threshold)	// 元素数量大于扩容阈值的话,就进行一次扩容
        resize();
    afterNodeInsertion(evict);	// 数据属于新增的后置操作,开发者可以重写这个方法,然后做自己想要完成的事情,比如打个日志什么的
    return null;
}

解析:此方法有5个入参,int hash表示当前key的特征值,K key, V value表示具体的数据,boolean onlyIfAbsent表示是否只允许新增,不允许覆盖,默认是false,也就是会对已有数据覆盖,boolean evict用在后置方法afterNodeInsertion中,HashMap中并没有具体实现。
实际的操作是首先通过运算获得元素应在容器的位置,若对应位置没有元素,说明新数据是第一个节点,直接创建节点放入就行了。若对应的位置已经有元素了,则判断这个节点类型是链表还是红黑树,如果是红黑树,直接调用红黑树的载入方法,如果是链表,则判断新数据的key是否已经存在,如果存在,覆盖即可,如果不存在,则判断新增元素之后是否达到树化阈值,达到的话对链表进行树化,没达到的就直接新增节点链接到链表末尾就可以了。
HashMap如何判断key是否相等呢:首先经历hash(key)方法算出来的特征值要一样(hashCode不一样,特征值是有可能一样的),其次需要满足a==b或者a.equals(b)

6.4 元素的查询

元素查询以V get(Object key)为例,来看HashMap如何通过Key找到目标元素:
源码:

public V get(Object key) {
	Node<K,V> e;
	// hash(key)算出特征值,然后调用getNode(int hash, Object key)找到对应的节点
	return (e = getNode(hash(key), key)) == null ? null : e.value; 
}

6.4.M.1 getNode(int hash, Object key)
源码:

final Node<K,V> getNode(int hash, Object key) {
	Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
	if ((tab = table) != null && (n = tab.length) > 0 && // table不为空,table的长度大于0,判定容器内是有元素
		(first = tab[(n - 1) & hash]) != null) { // 通过特征值与容量进行与运算得出下标,数组节点不为空判定这个节点有元素
		// 头元素总是要判断的
		//判断key是否一致,要求是 hash相等,并且==为true或者equals为true
		if (first.hash == hash && 
			((k = first.key) == key || (key != null && key.equals(k))))  
			return first;	// 相等说明找到这个元素了
		// 不相等继续往下找
		if ((e = first.next) != null) {
			// 如果是红黑树就用红黑树的方式往下找
			if (first instanceof TreeNode)
				return ((TreeNode<K,V>)first).getTreeNode(hash, key);
			// 不是红黑树就是链表,就遍历下去往下找
			do {
				// 判断相等的方法是一样的
				if (e.hash == hash &&
					((k = e.key) == key || (key != null && key.equals(k))))
					return e;	// key相等的话就返回节点
			} while ((e = e.next) != null);
		}
	}
	return null; // 没找到就返回null
}

解析:用key特征值加实际的key来找指定元素,先用特征值确定下标,然后在根据这个下标对应的是红黑树还是链表来使用响应的方法确定key的位置就行了,找不到就返回null。

6.5 HashMap的迭代

6.5.1 EntrySet迭代

HashMap的迭代,大多数情况都会推荐使用EntrySet进行迭代,这样会少一个根据Key重新查找Value的过程:

public Set<Map.Entry<K,V>> entrySet() {
    Set<Map.Entry<K,V>> es;
    return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}

可以看到调用entrySet()实际上返回的是一个entrySet的示例,属性es的存在仅仅是为了在重复调用时保持单例:

final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
    public final int size()                 { return size; }
    public final void clear()               { HashMap.this.clear(); }
    public final Iterator<Map.Entry<K,V>> iterator() {
        return new EntryIterator();
    }
    ... ...
}

只看EntrySet的iterator()方法,返回的是一个EntryIterator实例。

final class EntryIterator extends HashIterator
    implements Iterator<Map.Entry<K,V>> {
    public final Map.Entry<K,V> next() { return nextNode(); }
}

EntryIterator继承了HashIterator,并且重写了next()方法,且定义为final级别,此方法直接调用HashIterator中的nextNode()方法:

abstract class HashIterator {
	... ...
    final Node<K,V> nextNode() {
        Node<K,V>[] t;
        Node<K,V> e = next;
        if (modCount != expectedModCount)	// modCount检查
            throw new ConcurrentModificationException();
        if (e == null)	// 空校验
            throw new NoSuchElementException();
         // 如果当前位置树或者链表已经遍历完了,而且table不为空
        if ((next = (current = e).next) == null && (t = table) != null) {
        	// 下标后移,一直移动到下一个含有数据的位置,赋值新的next
            do {} while (index < t.length && (next = t[index++]) == null);
        }
        return e; // 返回之前的next,Node是继承自Entry的
    }
    ... ...
}

解析:HashMap的EntrySet使用的依然是HashMap的数据容器,而不是单独建立的一个数据容器。HashMap的最外层容器是一个数组,EntrySet的迭代器的迭代顺序就是从0开始,无论是红黑树结构(TreeNode)还是链表结构(Node),都维护了next属性,通过next属性向后遍历,到某尾后下标后移,遍历下一个元素节点。

6.5.2 可分割迭代

HashMap内部定义了分割迭代器,如HashMapSpliterator,KeySpliterator,ValueSpliterator,EntrySpliterator。其迭代顺序与EntrySet迭代迭代是一样的,不同的会是通过下标来界定范围。但是HashMap却没有提供任何获得这些分割迭代器的方法。这是因为严格来说Map只是一对key-value的映射,对HashMap的分割迭代实质上是针对于KeySet,ValueSet,EntrySet这样的数据集合。因此这些迭代器是被作为HashMap的KeySet,ValueSet,EntrySet的可分割迭代器来实现的。(HashMapSpliterator是一个底层实现,KeySpliterator,ValueSpliterator,EntrySpliterator都继承了他)。

总结

  1. HashMap允许Key和Value为null
  2. HahsMap线程不安全
  3. HashMap默认的初始容量是16,默认的装载因子是0.75,容量一定是2的幂次方,最大容量是1<<30
  4. HashMap使用拉链法应对hash冲突,底层使用数组+链表+红黑树实现。树化阈值为8,即当链表长度大于等于8时,链表就会转为红黑树。树退化阈值为6,就当红黑树元素数量小于等于6时,红黑树就会重新退化成链表
  5. HashMap通过对Object的hashCode进行计算获得的特征值与equals(或者==)方法判断对象是否相等,因此作为Key的类需要重新hashCode方法与equals方法,因为默认的hashCode方法是取的地址。
  6. HashMap是一个无序的map,这个无序不仅表示数据的存储顺序与插入顺序不同,同时也表示HashMap无法保证元素会一直处于插入时所在的位置而不会改变。

PS:
【JAVA核心知识】系列导航 [持续更新中…]
上篇导航:8: 从源码看LinkedList:一文看懂LinkedList
下篇导航:10: 从源码看HashSet:一文看懂HashSet
欢迎关注…

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yue_hu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值