Java 1.8-API源码学习之java.util.HashMap

 

目录

总结:

1 对象初始化常用构造方法

2 第一次调用 put 方法(使用空参构造方法创建的)

3 put

3.1 如果当前数组位置上Node的键与新的键相同

3.2 当前位置Node与新元素的key、hash不相同,且当前位置Node不是树节点

3.3 链表长度大于等于8,且数组长度大于等于64时,转化为红黑树

4 扩容

 


总结:

HashMap空参构造,只初始化了负载因子(0.75),其他成员变量均为默认值。

常用的有参构造方法 HashMap(int initialCapacity),是可以设置初始化大小的,在大概知道需要多大的map时,可以考虑使用这个构造方法。

HashMap 扩容:每次扩容至原来的2倍。

使用空参构造创建的对象,在第一次添加元素的时候,才会初始化一个长度为16的Node类型的数组。

链表转红黑树的时机:链表长度大于8 , 数组长度大于64

红黑树转链表的时机:链表程度小于 6

HashMap 允许空值作为键和值

HashMap 是无序,且键不重复的

HashMap 线程不安全,多线程操作下可能会抛出 ConcurrentModificationException

1 对象初始化常用构造方法

  • HashMap()
// 负载因子(阈值 = 数组长度 * 负载因子),阈值时数组扩容的临界值
static final float DEFAULT_LOAD_FACTOR = 0.75f;

/**
 * 构造一个具有默认的初始容量(16)和默认的负载因子(0.75)的空的HashMap
 * (在使用这个构造方法new对象时,并没有创建数组,而是在第一次调用put方法的时候才创建)
 */
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // 只初始化负载因子,其它字段使用默认值
}
  • HashMap( int initialCapacity )
/**
 * 构造一个具有指定的初始容量和默认的加载因子(0.75)的空的HashMap
 *
 * @param  initialCapacity 指定初始容量
 * @throws IllegalArgumentException 如果指定的初始容量是负的
 */
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR); // 调用两个参数的构造方法
}

2 第一次调用 put 方法(使用空参构造方法创建的)

  • 总体调用链路代码图示

  • 源码详细解析
public class HashMap {
    // [......]

    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    // [......]

    public HashMap() {
        // 初始化 负载因子  默认是0.75
        this.loadFactor = DEFAULT_LOAD_FACTOR;
    }

    // 第一次调用 put 方法
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

    /**
     * 实现了 Map.put 及相关方法
     *
     * @param hash         key 的 hash 值
     * @param key          键
     * @param value        值
     * @param onlyIfAbsent 如果是true,就不修改已存在的值
     * @param evict        如果是false,table 就是创建模式
     * @return 旧值,如果没有就返回null
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
        Node<K, V>[] tab;
        Node<K, V> p;
        int n, i;

        // tab=null; 条件成立,进入
        if ((tab = table) == null || (n = tab.length) == 0)
            // tab = 长度为16的数组;     n = 16;
            n = (tab = resize()).length;

        // i=hashCode        p = tab[hashCode]   此处数组中还全部为null,所以 p == null 成立
        if ((p = tab[i = (n - 1) & hash]) == null)
            // 创建节点,并将其放入bucket(数组)中
            tab[i] = newNode(hash, key, value, null);
        else {
            // [......]
        }
        ++modCount;  //此HashMap在结构上修改的次数
        if (++size > threshold) // size=1;  threshold=12;   size > threshold 不成立
            resize();
        afterNodeInsertion(evict); // LinkedHashMap 的回调函数(此处先不需要管)
        return null; // 元素直接填入数组中,所以不存在旧值
    }

    // 创建一个常规、非树节点
    Node<K, V> newNode(int hash, K key, V value, Node<K, V> next) {
        return new Node<>(hash, key, value, next);
    }

    // 初始化、扩容等操作的方法
    final Node<K, V>[] resize() {
        Node<K, V>[] oldTab = table;  // oldTab = null; 现在table数组还没有初始化
        int oldCap = (oldTab == null) ? 0 : oldTab.length; // oldTab==null 成立   =>  oldCap=0;
        int oldThr = threshold; // oldThr = threshold = 0
        int newCap, newThr = 0;
        if (oldCap > 0) { // oldCap目前 = 0  不成立
            // [......]
        } else if (oldThr > 0) // oldThr目前 = 0   不成立
        // [......]
        else{ // 最终走的是这个分支:初始阈值为零表示使用默认值
            newCap = DEFAULT_INITIAL_CAPACITY;  // newCap=16
            newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // newThr = 0.75 * 16 = 12
        }
        if (newThr == 0) { // 现在 newThr=12  条件不成立
            // [......]
        }
        threshold = newThr;  // threshold=12
        @SuppressWarnings({"rawtypes", "unchecked"})
        Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap];  // 创建数组(bucket)
        table = newTab; // table=数组, 到这里,长度为16的数组才初始化完毕
        if (oldTab != null) { // oldTab是=null的,这里不成立
            // [......]
        }
        return newTab; // 返回 新数组
    }

    // Node 节点,存储在数组与链表中(红黑树中存储的是TreeNode)
    static class Node<K, V> implements Map.Entry<K, V> {
        final int hash;
        final K key;
        V value;
        Node<K, V> next;

        Node(int hash, K key, V value, Node<K, V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        // [......]
    }
}
  • 过程图示

(演示中的hash值均为假设值)

调用 new HashMap() 的时候,还没有创建数组。

在第一次调用put方法的时候,比如说:map.put( 1, "abc" );

创建一个Node节点:

创建一个长度为 16 的 Node 类型的数组:

根据公式计算出新Node节点的位置:(16 - 1) & hash = 4 , 将新结点放入数组中

3 put

3.1 如果当前数组位置上Node的键与新的键相同

  • 源码详细解析:
public class HashMap {
    // [......]

    // 调用此方法(假设)
    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)
        // [......]
		else{
            Node<K, V> e;  // 用来记录是否存在key对应的Node节点,不存在,此值一直为null
            K k;
            // p 现在是数组当前位置上的node节点,hash 是要添加的元素的key的hash值
            // 1、p.hash == hash && ((k = p.key) == key  : 如果两个Node节点是相同的,则为 true
            // 2、(key != null && key.equals(k)))        : 如果两个Node节点的key是相同的,则为 true
            if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
                e = p; //进来了,说明当前位置上存在key对应的Node节点p
            else if (p instanceof TreeNode)
            // [......]
			else{
                // [......]
            }
            if (e != null) { // 现有的key映射  此时e!=null满足
                V oldValue = e.value; // 记录原值
                // 判断如果onlyIfAbsent是false  或者 原值是null , 就修改原值
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;  // 修改值
                afterNodeAccess(e);
                return oldValue; // 返回原值
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

    // [......]
}
  • 过程图示:

1、数组中当前状态:

2、map.put( "123", "def" ); 

3、修改值

注意:在上述过程中,没有产生新的 Node 对象。

3.2 当前位置Node与新元素的key、hash不相同,且当前位置Node不是树节点

  • 源码详细分析:
public class HashMap {
    // [......]

    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)
            // [......]
            if ((p = tab[i = (n - 1) & hash]) == null)
            // [......]
        else {
            Node<K, V> e;
            K k;
            if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
            // [......]
            else if (p instanceof TreeNode)
            // [......]
            else{
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) { // 循环遍历链表,如果当前节点的next指针为null ,就将新节点挂载到当前节点
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // 判断是否转为红黑树,这里先不深入
                            treeifyBin(tab, hash);
                        break;
                    }

                    // 判断键是否相同、是否是同一个Node节点,,和上一层的if的目的相同
                    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // 现有的key映射  此时e!=null满足
                V oldValue = e.value; // 记录原值
                // 判断如果onlyIfAbsent是false  或者 原值是null , 就修改原值
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;  // 修改值
                afterNodeAccess(e);
                return oldValue; // 返回原值
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

    // [......]
}
  • 过程图示:

1、数组中当前状态

2、map.put( "56789",  "aaa" );

3、创建新节点并挂载

3.3 链表长度大于等于8,且数组长度大于等于64时,转化为红黑树

源码加图解析:

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)
        // [......]
        if ((p = tab[i = (n - 1) & hash]) == null)
        // [......]
    else {
        Node<K, V> e;
        K k;
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
        // [......]
        else if (p instanceof TreeNode)
        // [......]
        else{
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) { // 循环遍历链表,如果当前节点的next指针为null ,就将新节点挂载到当前节点
                    p.next = newNode(hash, key, value, null); // 挂载新节点
                    if (binCount >= TREEIFY_THRESHOLD - 1) // 判断是否转为红黑树 TREEIFY_THRESHOLD=8 
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        // [......]
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

在链表长度大于等于8的时候,调用treeifyBin(tab, hash)方法:

假设当前HashMap存储元素如下:

调用 treeifyBin(tab, hash)方法:

//替换指定哈希表的索引处bin中的所有链接节点,除非表太小,否则将改为红黑树。
final void treeifyBin(Node<K, V>[] tab, int hash) {
    int n, index;
    Node<K, V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) // MIN_TREEIFY_CAPACITY=64
        resize();  // 如果hash表为null 或者 长度不足64   就不转为红黑树;扩容机制后面再说
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K, V> hd = null, tl = null;
        do {
            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);
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

// 创建一个树节点
TreeNode<K, V> replacementTreeNode(Node<K, V> p, Node<K, V> next) {
    return new TreeNode<>(p.hash, p.key, p.value, next);
}

// 树节点
static final class TreeNode<K, V> extends LinkedHashMap.Entry<K, V> {
    // final int hash; 继承自HashMap.Node
    // final K key;    继承自HashMap.Node
    // V value;        继承自HashMap.Node
    // Node<K,V> next; 继承自HashMap.Node
    // Entry<K,V> before, after;   继承自LinkedHashMap.Entry

    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
    boolean red;

    TreeNode(int hash, K key, V val, Node<K, V> next) {
        super(hash, key, val, next);
    }
}

我们先将关注点放在这个链表上:

现在先用图详细说一下do while循环中的操作做了什么:

第一次循环:

第二次循环:

第三次循环:

以此类推,最后一次循环之后:循环结束,实际上是创建了一个TreeNode节点的双向链表。

 

tab[index] = hd 这句代码,将整条TreeNode节点构成的双向链表头放到数组中去。

执行 hd.treeify( tab ) 这个方法,就是将双向链表转换为红黑树。

双向链表转红黑树就不继续剖析了,后续会专门对红黑树结构进行总结。

可以参考:https://blog.csdn.net/weixin_40255793/article/details/80748946

 

还剩下一些红黑树的操作没有详细展开,后续专门对红黑树进行总结的时候再进行补充。

 

4 扩容

// 扩容  这里分析第一次扩容  假如现在是插入第13个元素
final Node<K, V>[] resize() {
    Node<K, V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;  //oldCap=16 
    int oldThr = threshold; // oldThr=12
    int newCap, newThr = 0;
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {  // MAXIMUM_CAPACITY=1<<30
            // [......]
        } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                oldCap >= DEFAULT_INITIAL_CAPACITY)
            // newCap=oldCap<<1 = 16 * 2 = 32    每次扩容为原来的2倍
            // oldCap(16) >= DEFAULT_INITIAL_CAPACITY(16)  成立
            newThr = oldThr << 1; // newThr=24
    } else if (oldThr > 0)
    // [......]
else{
        // [......]
    }
    if (newThr == 0) {
        // [......]
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes", "unchecked"})
    Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap]; // new 一个长度为32的数组
    table = newTab;
    if (oldTab != null) {  // 如果原hash表不为null,就将旧的HashMap中的所有元素分配到新的HashMap
        for (int j = 0; j < oldCap; ++j) {  // 遍历处理每一个桶
            java.util.HashMap.Node<K, V> e;
            if ((e = oldTab[j]) != null) {  // 取出当前桶中的元素
                oldTab[j] = null; // 将原桶位置引用置为null
                if (e.next == null) //如果当前桶中只有一个元素,直接重新hash
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof java.util.HashMap.TreeNode) // 如果是红黑树,就切分成两个红黑树
                    ((java.util.HashMap.TreeNode<K, V>) e).split(this, newTab, j, oldCap);
                else { // 如果是单链表,就拆分成2个单链表
                    java.util.HashMap.Node<K, V> loHead = null, loTail = null;  // 原索引处的一个链表
                    java.util.HashMap.Node<K, V> hiHead = null, hiTail = null;  // 原索引+oldCap的一个链表
                    java.util.HashMap.Node<K, V> next;
                    do {
                        next = e.next;
                        // 根据hash值的某一位为0还是1将单链表拆分
                        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;
}

注意:

如果是单个节点,直接重新计算位置放下即可。

如果是链表,则根据 e.hash & oldCap 是0还是1分成两个单链表,一个放在原索引位置,一个放在 index+oldCap 位置。

如果是红黑树,则根据  e.hash & bit 是0还是1分成两个红黑树,一个放在原索引位置,一个放在 index + bit 位置。如果红黑树元素小于6了,就会转换成链表。

上述操作是 HashMap 的优化部分,在每一次扩容后,可以将原素分散开放在 HashMap 中。

 

源码分析参考:https://blog.csdn.net/weixin_40255793/article/details/80748946

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值