欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java容器之HashMap源码分析(妈妈再也不用担心我不懂HashMap了)

程序员文章站 2022-07-14 11:20:02
...

  最近面试被问HashMap容器的实现原理,答的一塌糊涂。。。虽说一直念叨着说要看看Java容器的源码,但总是被耽搁了,今天终于静下心来看了????‍♂️。

  注明:以下源码分析都是基于jdk 1.8.0_221版本
Java容器之HashMap源码分析(妈妈再也不用担心我不懂HashMap了)

一、HashMap概述(一图以蔽之

  HashMap的类声明如下

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

  HashMap是一个<key,value>(或称键值对)容器,其底层实现是使用一个hash数组指向多个不同的链表。每次我们放入一个<key,value>,它会自动计算key对应的hash值,然后根据hash值插入到不同的链表中。
Java容器之HashMap源码分析(妈妈再也不用担心我不懂HashMap了)

  注明:可能会有人对为啥要用hash数组链表产生疑问,这是因为实际插入过程中会出现多个<key,value>的key计算出的hash值相同(哈希冲突),如上图的table[1]。但是当链表太长时,在容器中查找<key,value>,每次都要遍历耗时长,降低了查找效率,所以在Java 8中,引入了红黑树。默认当某个hash值下超过了8个<key,value>,此时就需要转化成红黑树,如果上图中的table[14]

二、HashMap类的属性

1、HashMap类静态属性

/**
 * 序列化的版本号
 */
private static final long serialVersionUID = 362498820763181265L;
/**
 * 默认的初始化容量大小,并且必须是2的幂(主要是考虑效率,后面有介绍)
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

/**
 * 最大的容量(容器中存放<key, value>的最大数量)
 */
static final int MAXIMUM_CAPACITY = 1 << 30;

/**
 * 负载因子
 * 当容器中<key, value>的数量超过capacity * DEFAULT_LOAD_FACTOR时,需要扩容
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;

/**
 * 链表转红黑树阈值
 * 当某个hash值下<key, value>用链表存储,并且链表长度不小于该值,就需要转成红黑树
 */
static final int TREEIFY_THRESHOLD = 8;

/**
 * 红黑树转链表阈值
 * 当某个hash值下<key, value>是用红黑树存储,并且树中的节点数小于该值,就需要转成链表
 */
static final int UNTREEIFY_THRESHOLD = 6;

/**
 * 最小树形化容量阈值
 * 当哈希表中的容量 > 该值时,才允许将链表转成红黑树操作,否则直接扩容。
 * 为了避免进行扩容、链表转红黑树选择的冲突,并且这个值不能小于 4 * TREEIFY_THRESHOLD(链表转红黑树阈值)
 */
static final int MIN_TREEIFY_CAPACITY = 64;

2、HashMap非静态属性

  transient关键字的作用是在序列化的时候排除该属性,比如写入硬盘持久化,用这个关键字修饰的属性在对象保存时不会写入。(不过HashMap类在尾端重写了序列化方法,手动指定了需要序列化的属性)

/**
 * table数组,也称hash桶数组
 */
transient Node<K,V>[] table;

/**
 * entrySet属性,把<K,V>存放到Set容器中(一般hashmap的遍历用此属性)
 */
transient Set<Map.Entry<K,V>> entrySet;

/**
 * 容器key-value数量(注意与容器的容量(容器可存放的数量)不同)
 */
transient int size;

/**
 * 容器进行结构性调整(增加或者删除键值对等操作,不包括修改value值)的次数
 */
transient int modCount;

/**
 * 容器中能容纳的key-value极限,capacity * loadFactor,超过就需要扩容
 */
int threshold;

/**
 * 负载因子,默认是0.75(前面类的静态属性已经定义过了)
 */
final float loadFactor;

\color{red}注意:上面提到的容量就是table数组的长度,size是容器中存放的key-value数量,threshold = 容量 * 负载因子,表示的该容器最多可以放置多少个key-value

三、HashMap类的构造器

  查看HashMap类文件,可以发现一共有4个构造器。

/**
 * @param  initialCapacity 初始化容量大小
 * @param  loadFactor      负载因子
 * @throws IllegalArgumentException if the initial capacity is negative
 *         or the load factor is nonpositive
 */
public HashMap(int initialCapacity, float loadFactor) {
	// 检查initialCapacity的合法性
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
    // 检查initialCapacity是否超过了可设置的最大容量(类静态属性)
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    // 检查loadFactor负载因子的合法性
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    //初始化threshold稍微复杂一点,tableSizeFor方法解析见本博客尾端
    this.threshold = tableSizeFor(initialCapacity);
}

/**
 * @param  initialCapacity 初始化容量大小
 * @throws IllegalArgumentException if the initial capacity is negative.
 */
public HashMap(int initialCapacity) {
	//默认负载因子为0.75
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

/**
 * 只设置负载因子为0.75,其它值全部默认
 */
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
}

/**
 * 复制构造函数,将另外一个map初始化构造
 *
 * @param   m 其它map容器
 * @throws  NullPointerException
 */
public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    //将m容器中的所有entity放入新建的容器对象中
    putMapEntries(m, false);
}

四、增加key-value相关方法

1、put方法

  put方法,往容器中添加key-value,允许key = null,也允许value = null

/**
 * 往容器中添加`key-value`,允许`key = null`,也允许`value = null`
 */
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

2、putVal方法

  putVal方法的作用是往map容器中插入一个key-value

/**
 * Implements Map.put and related methods.
 *
 * @param hash key的hash值(调用hash()方法)
 * @param key 插入键值对key
 * @param value 插入键值对value
 * @param onlyIfAbsent 设为true时,表示如果容器已经存在这个key就不进行修改
 * @param evict 为 false时,表示容器正处于创建(其它map传入初始化)
 * @return previous value, or null if none
 * 
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    // tab指向 对象的table数组(hash桶数组),p 指向hash对应的桶
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 如果容器为空,则需要调用resize方法,初始化table数组
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 将p指向hash对应的hash桶
    if ((p = tab[i = (n - 1) & hash]) == null)
    	// (n - 1) & hash求出hash对应的table数组下标,如果这个位置为空,说明这个桶为空
    	// 直接放入table中,不需要生成链表、红黑树等
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        // 根据p(指向对应的hash桶),找到key对应的节点位置
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
        	// 如果当前p指向桶第一个元素就是key
            e = p;
        else if (p instanceof TreeNode)
        	// 如果p指向的内容(hash桶对应的第一个元素)是红黑树的对象,说明该桶已转换为红黑树,调用putTreeVal插入
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
        	// 否则桶内实现是链表,只能遍历链表查找key
            for (int binCount = 0; ; ++binCount) {
            	// p.next == null。即链表的尾端
                if ((e = p.next) == null) {
                	// 还没找到,则需要插入节点
                    p.next = newNode(hash, key, value, null);
                    // 如果该桶的元素超过了 TREEIFY_THRESHOLD,需要进行扩容或者转成红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                //如果当前节点以及成功匹配key,退出
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) {
        	//找到了key对应的位置,再赋value
            V oldValue = e.value;
            // onlyIfAbsent入口参数,为true,则不更新value(前面已说明)
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            //成功更新了节点
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    //容器中的key-value数自增,并且判断是否需要扩容(前面已多次说明threshold = 容器容量 * 最大负载因子)
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

3、putMapEntries方法

  putMapEntries方法作用是将其他map容器中的key-value复制到本容器中。

/**
 * 将一个map容器中的key-value复制到本容器
 *
 * @param m 其它map容器
 * @param evict 为 false时,表示容器正处于创建(其它map传入初始化)
 */
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    int s = m.size();
    //只有当m非空的时候才有遍历的必要
    if (s > 0) {
    	//如果table为空,则需要判断m中的元素个数是否会超过本容器可容纳的数量(容器容量 * 负载因子)
        if (table == null) { // pre-size
        	//由于table为空,我们需要将 m.size() / 负载因子loadFactor,得到需要的最小空间
            float ft = ((float)s / loadFactor) + 1.0F;
            int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY);
            //最小空间都大于容器当前能存放的最大数量(threshold = 当前容量 * 负载因子)
            if (t > threshold)
            	//当前容器无法容纳,则需要计算不必t小的2的幂
                threshold = tableSizeFor(t);
        }
        else if (s > threshold)
        	// table != null,此时我们只要判断 m.size() > 容器当前能存放的最大数量(threshold = 当前容量 * 负载因子),从而决定是否扩容
            resize();
        // 经过上面的扩容操作,已经保证 m.size() < 容器当前能存放的最大数量(threshold = 当前容量 * 负载因子),遍历m放入本容器即可
        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);
        }
    }
}

五、删除key-value相关方法

1、remove方法

  remove方法是提供给外界的删除key-value的接口。

/**
 * 根据key删除key-value,删除成功返回对应的value,否则返回null
 */
public V remove(Object key) {
    Node<K,V> e;
    //调用removeNode方法
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

2、removeNode方法

  removeNode方法是删除key-value的具体实现(注意不对外展示)。

/**
 * Implements Map.remove and related methods.
 *
 * @param hash key对应的hash(调用hash()方法即可计算得到)
 * @param key 待删除的key-value对应的key
 * @param value key-value对应的value,只有当matchValue == true时,此参数才有意义
 * @param matchValue 如果设为true,删除的时候还需要匹配value才能删
 * @param movable 设为false,表示删除成功了不移动其它节点(一般设为true,即删除节点后需要进行调整)
 * @return 删除成功则返回key对应的value,否则返null
 */
final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) {
	// tab用于指向table数组(hash桶数组),p用于指向传入的key对应的hash所对应的hash桶
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    // 如果table数组存在hash对应的桶
    if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) {
    	// node的作用是指向key对应的节点位置
        Node<K,V> node = null, e; K k; V v;
        // 判断hash桶的第一个元素的key是否是匹配成功
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        else if ((e = p.next) != null) {
        	// 匹配不成功,则需要判断这个桶是链表还是红黑树实现
            if (p instanceof TreeNode)
            	// 如果是 红黑树则调用getTreeNode方法
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else {
            	// 否则只能遍历链表
                do {
                    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        // 如果node != null,则说明容器中存在key对应的key-value
        // 如果 matchValue == true,则还需要匹配value,才能删
        if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) {
            // 如果node指向的对象是TreeNode类型,则调用红黑树对应的remove方法即可
            if (node instanceof TreeNode)
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            else if (node == p)
            	// 如果node是table数组中的元素(hash桶内第一个元素)
                tab[index] = node.next;
            else
            	// 否则node是链表中的其他节点
                p.next = node.next;
            // 删除节点是结构性调整
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

六、查找key-value相关方法

1、get方法

  get方法的作用是根据key查找value

/**
 * 根据`key`查找`value`。
 * @see #put(Object, Object)
 */
public V get(Object key) {
    Node<K,V> e;
    //调用getNode方法查找
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

2、getNode方法

  getNode方法的作用是根据key查找查找<key, value>

/**
 * 根据`key`查找查找`<key, value>`
 *
 * @param hash key对应的hash值(调用hash()方法即可获取)
 * @param key the key
 * @return the node, or null if none
 */
final Node<K,V> getNode(int hash, Object key) {
	// tab用于指向table数组(hash桶数组),first用于指向hash桶第一个元素,e用于指向hash、key匹配到的节点位置
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    //查找table数组(hash桶数组)中是否存在hash对应的桶
    if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
        //判断桶中的第一个元素first的key与需要查找的key是否想相同
        if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        //这个桶不能只有一个元素, > 1才有继续寻找的必要
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
            	//如果该hash桶是红黑树实现,调用红黑树对应的查找方法
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            //否则该hash桶是链表实现,遍历链表即可
            do {
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

3、containsKey方法

  containsKey方法的作用是判断容器中是否存在key对应的key-value

/**
 * 判断容器中是否存在`key`对应的`key-value`
 */
public boolean containsKey(Object key) {
	// 调用getNode方法,如果查找到了key-value则说明存在
    return getNode(hash(key), key) != null;
}

4、containsValue方法

  containsValue方法的作用是判断容器中是否存在value对应的key-value

/**
 * 判断容器中是否存在`value`对应的`key-value`
 */
public boolean containsValue(Object value) {
	// tab用于指向table数组(hash桶数组)
    Node<K,V>[] tab; V v;
    if ((tab = table) != null && size > 0) {
    	//遍历table数组(hash桶数组)
        for (int i = 0; i < tab.length; ++i) {
        	// 遍历hash桶中的每一个元素
            for (Node<K,V> e = tab[i]; e != null; e = e.next) {
                if ((v = e.value) == value ||
                    (value != null && value.equals(v)))
                    return true;
            }
        }
    }
    return false;
}

七、其它方法

1、tableSizeFor方法

  该方法用于找出不小于cap的最小2的幂。
  假设cap 的二进制形式为01xxxx...xxx,先考虑n = cap的情况.

n n >>> x n | = n >>> x
初始化为cap = 01xxxx...xxx n >>> 1结果 001xxx...xxx 011xxx...xxx
011xxx...xxx n >>> 2结果 00011x...xxx 01111x...xxx
01111x...xxx n >>> 4结果 000001111xxx 011111111...

  表格所表达的意思就是依次将cap最高位为1后面的所有位都置为1,第一次右移一位,n |= n >>> 1得到了两个1,第二次n |= n >>> 2,右移两位,得到了4个1,然后右移4位,得到了8个1…
  然后n += 1,也就是二进制011...111进位100...000,正好是2的幂。

/**
 * 返回不小于cap的最小的2的幂,比如cap == 3时,返回4,cap == 12时返回16等等
 */
static final int tableSizeFor(int cap) {
	// n = cap - 1是为了防止当cap本身就是2的幂,此时计算出的结果偏大了
	// 比如cap = 16(二进制”10000“),通过计算求得的n = "11111”,
	// 然后n + 1 = "100000" = 32, 实际上cap自身16就是解
    int n = cap - 1;
    // >>> 运算符的作用是 无符号右移(左边填充0),比如 ’11111‘ >>> 1的结果为'01111'
    // 如果你知道原码、补码的相关知识,这点很容易理解,不知道就先记着吧
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    //最后返回n + 1、MAXIMUM_CAPACITY(HashMap容器容量最大值)的较小值,
    //由于n计算时可能发生了溢出,所以需要判断是否小于0
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

  如果你还是没看懂这个解释,可以随便带入几个cap,比较计算后的二进制结果就会发现这个方法的作用。

2、hash方法(重要)

  此方法用来计算key对象的hash值,从而决定放到table表hash桶)的哪个表项中。

/**
 * 计算key对象的hash值
 */
static final int hash(Object key) {
    int h;
    // hashCode是Object类的方法(一般重写tostring方法会让你重写这个方法)
    // 在HashMap容器中,hash值 == key.hashCode()的前16位 异或 key.hashCode()的后16位(主要是防止hash冲突,多个key的hash值一样)
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

3、resize方法(重要)

  resize方法的作用是扩容。

/**
 * Initializes or doubles table size.  If null, allocates in
 * accord with initial capacity target held in field threshold.
 * Otherwise, because we are using power-of-two expansion, the
 * elements from each bin must either stay at same index, or move
 * with a power of two offset in the new table.
 *
 * @return the table
 */
final Node<K,V>[] resize() {
	// 记录扩容前的状态
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    // 如果oldCap > 0,说明不是初始化
    if (oldCap > 0) {
    	// 如果oldCap 不小于HashMap容器定义的最大容量,修改threshold为最大值
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // oldCap * 2后是否超过 MAXIMUM_CAPACITY
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 容量变为原来的2倍,可存放的阈值(最大容量 * 负载因子)也 * 2,
            // 由于容量 * 2,所以阈值也需要 * 2
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0)
    	// 根据前面判断知oldCap <= 0,此时时调用了HashMap的带参构造器,初始容量用threshold替换,
        //在带参构造器中,threshold的值为 tableSizeFor() 的返回值,也就是2的幂,而不是 capacity * load factor
        newCap = oldThr;
    else {
    	// 根据前面两个判断,oldCap <= 0 且 oldThr > 0,即调用了默认构造器
    	// 此时容器容量 newCap 赋值默认初始化容量,
    	// 容器最大存放数量newThr 赋值 默认负载因子 * 默认初始化容量
        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"})
    // 创建新的 table数组(hash桶数组)
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
    	//将oldTab中的所有key-value复制到newTab,遍历oldTab数组(hash桶数组)
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            //当前hash桶不为空才有遍历的必要
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                // 如果该hash桶中国只有一个元素,直接复制
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                // 如果该hash桶是红黑树实现,调用split方法复制
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else {
                	// 否则该hash桶是链表实现,需要遍历链表
                	// 将源链表拆分根据成两个链表,原链表中的所有节点(Node.hash % oldCap) == 0
                	// loHead、loTail指向第一个链表的头、尾,链表中的(Node.hash & oldCap) == 0
                	// hiHead、hiTail指向第二个链表的头、尾,链表中的(Node.hash & oldCap) != 0
                    
                    // 比如 oldCap = 16时,hash = 13,29,45,61...都应该放在oldTab[12]这个桶下
                    // 先应 newCap = 2 * oldCap = 32,需要拆分成newTab[12]、newTab[12 + oldCap = 28]两个桶
                    // newTab[12] = [13, 45],  hash % newCap = hash % 32 = 13
                    // newTab[12 + oldCap = 28] = [29, 61], hash % newCap = hash % 32 = 28
                    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);
                    //然后分别将第一个链表放入newTab[j]
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    //第二个链表放入newTab[j + oldCap]
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

4、treeifyBin方法(重要)

  treeifyBin方法是将某个链表实现的hash桶转换为红黑树。

/**
 * @parm hash 代转换成红黑树的hash桶对应的hash值
 */
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    // 如果容器的(长度)容量小于 MIN_TREEIFY_CAPACITY,则直接扩容
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    // 否则 hash对应的 桶不为空时,此时进行链表转红黑树操作
    else if ((e = tab[index = (n - 1) & hash]) != null) {
    	// (n - 1) & hash的作用是获取hash桶对应的下标(table数组),效果等同于 hash % n(n 是 tab数组的长度),
    	// 这是由于n 是 2 的次幂,这也是为什么table的容量(长度)必须初始化为2 的次幂,简化求余操作
        TreeNode<K,V> hd = null, tl = null;
        // 遍历 tab[(n - 1) & hash],将所有节点都转成 TreeNode 节点
        do {
        	// 将当前节点转换为 TreeNode 节点(注意此处并没转成红黑树)
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        // 然后在调用treeify方法,将hd链表转成红黑树
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

八、总结(一图以蔽之)

Java容器之HashMap源码分析(妈妈再也不用担心我不懂HashMap了)
  HashMap由一个table数组hash桶数组)和若干个链表组成,当某个hash桶内的数量过多时(链表太长,查找效率低),此时需要将链表转结构成红黑树结构,默认是链表长度超过8就要转换(当然如果红黑树中的节点太少,默认是 < 6时,需要转换回链表结构)。每次插入、删除节点,只要维持table数组、各个链表、红黑树即可。

已更新 Java容器之Hashtable源码分析,各位小伙伴可以对比着看,效果更佳哦~

相关标签: Java