HashMap JDK1.8源码详解

原理:数组 + 链表 + 红黑树

构造方法
static final float DEFAULT_LOAD_FACTOR = 0.75f;	//负载因子默认0.75
//无参构造
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; 	//初始化负载因子
}
put(K key, V value)方法

1.7HashMap进行put时采用头插法,在多线程扩容时,由于采用头插法会导致链表反转,指针需要转移,在此期间可能存在指针改变时导致链表死循环。
1.8HashMap采用尾插法,扩容时,还是链表元素保持原来顺序,无需改变指针,从而规避了死循环,但是多线程下还是数据丢失覆盖问题,由于没有加锁。

// 向hashMap中添加元素
public V put(K key, V value) {
    //1.调用hash(key)计算key的hash值
    //2.key
    //3.value
    //4.false: 是否更改HashMap现有的value值
    //5.true:不是创建模式
    return putVal(hash(key), key, value, false, true);
}
hash()方法
// 计算key的再hash值
static final int hash(Object key) {
    int h;
    //如果key==null,返回0
    //如果key!=null,①获取key的hashCode值,然后②将key的hashCode值进行无符号右移16位,将两者进行异或
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

为什么要右移16位?

为了减少碰撞,进一步降低hash冲突的几率。int类型的数值是4个字节的,右移16位异或可以同时保留高16位于低16位的特征。

为什么要进行异或运算?

首先将高16位无符号右移16位与低十六位做异或运算。如果不这样做,而是直接做&运算那么高十六位所代表的部分特征就可能被丢失 将高十六位无符号右移之后与低十六位做异或运算使得高十六位的特征与低十六位的特征进行了混合得到的新的数值中就高位与低位的信息都被保留了 ,而在这里采用异或运算而不采用& ,| 运算的原因是 异或运算能更好的保留各部分的特征,如果采用&运算计算出来的值会向0靠拢,采用|运算计算出来的值会向1靠拢。

总的来说就是我们平时使用的HashMap的容量有限,大部分都是小于2^16=65536这个数字的,然后计算元素的下标时,总是拿(hash值&容量)计算出下标,这会导致key的hash值高16位都没有使用到,进行增加hash冲突的几率,所以进行(h = key.hashCode()) ^ (h >>> 16),将hash值的高16位与低16位进行混合,能更好的保留各部分的特征,减少hash冲突。


putVal()方法
/**
 * Map.put的实现
 * @param hash key的哈希值 
 * @param key 放入hash表的键
 * @param value 放入hash表的值
 * @param onlyIfAbsent 如果为true,不修改存在的值value
 * @param evict 如果为false,哈希表属于创建模式.
 * @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;
    //如果hash表为空,或者hash表长度==0,则进行扩容
    if ((tab = table) == null || (n = tab.length) == 0)
        //初始化哈希表,进行扩容
        n = (tab = resize()).length;
    
    // n:是hash表的长度,根据hash值和(n-1)计算新增的元素需要放入hash表的下标i
    // 然后判断hash表的[i]位置如果没有元素存在,则新创建一个节点存入该位置
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        // 如果tab[i]位置存在元素p,则需要进一步判断
        Node<K,V> e; K k;
        // 如果该桶的第一元素p和插入的元素key相同,则更新value
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)
            // 如果p节点是一个树的节点,则将该节点插入到树中
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            // 如果第一个元素不相等且不是树节点,则遍历链表
            for (int binCount = 0; ; ++binCount) {
                // 遍历到链表尾部,则插入新节点
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 插入后判断链表的长度是否大于等于7,如果大于等于,则尝试将链表转为红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        // 尝试将链表转为红黑树,如果tab!=null && tab.length>=64,才转为红黑树
                        // 否则只是扩容
                        treeifyBin(tab, hash);
                    break;
                }
                // 如果遍历链表存在key相同的元素,则退出循环 
                // hash表的key是通过hashCode()和equals()来区分的
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        // 如果hash表存在key相同的元素,则将旧的value值替换为新的value值,然后返回旧的值
        if (e != null) { 
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            // LinkedHashMap的回调函数
            afterNodeAccess(e);
            return oldValue;
        }
    }
    // 记录HashMap的修改次数
    // 作用:为了避免在迭代的过程中不变动HashMap的结构,当HashMap的结构发生变动,则抛出异常
    ++modCount;
    // 如果hash表的实际元素大于阈值,则进行扩容
    if (++size > threshold)
        resize();
    // LinkedHashMap的回调函数
    afterNodeInsertion(evict);
    return null;
}
resize()方法
/*
创建新的桶数组:首先,resize() 函数会根据当前容量和负载因子计算出新的容量。新的容量是原来容量的两倍。然后,它会创建一个新的桶数组,长度为新的容量。

迁移元素:接下来,resize() 函数会遍历原来的桶数组,将其中的元素重新分配到新的桶数组中。它会根据键的哈希码重新计算新的桶索引,并将元素插入到对应的桶中。

注意,由于新的桶数组长度是原来的两倍,所以每个元素的新桶索引要么保持不变,要么增加原来桶数组长度的值。这是因为新的桶数组的长度是原来的两倍,所以新的桶索引要么是原来的索引,要么是原来的索引加上原来桶数组的长度。

在迁移元素的过程中,如果某个桶中的元素形成了链表或红黑树,resize() 函数会保持原有的数据结构不变。只有当某个桶中的元素个数小于 TREEIFY_THRESHOLD(8)时,才会将红黑树转换为链表。

更新容量和阈值:最后,resize() 函数会更新 HashMap 的容量和阈值。容量更新为新的容量,阈值更新为新的容量乘以负载因子。
*/

//如果哈希表为空,则初始化,否则进行扩容
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    //获取旧的哈希表容量
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //获取哈希表旧的实际容量(capacity * load factor),即阈值,实际容量
    int oldThr = threshold;
    int newCap, newThr = 0;
    //如果哈希表不为空,hashMap已存在,进行扩容,会进入这里的逻辑
    if (oldCap > 0) {
        // 旧容量很大,大于 1<<30,则将旧容量oldCap=Integer.MAX_VALUE
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            // 结束,hashMap已经达到最大值,无法扩容了
            return oldTab;
        }
        // 旧容量的2倍小于1<<30,并且  旧容量大于等于初始容量16,则将旧阈值扩容为原来的2倍
        // 此处的(newCap = oldCap << 1)就是hash表的扩容为原来的2倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // 2倍阈值
    }
    else if (oldThr > 0) // 如果哈希表旧的实际容量 > 0
        // 如果在创建HashMap时使用的构造方法是传入初始容量的的那个,会进入这里的逻辑
        // 在构造方法中会根据传入的容量initialCapacity,计算threshold,就是计算大于等于
        // initialCapacity的2次幂,如:
        // initialCapacity == 15   threshold = 16
        // initialCapacity == 16   threshold = 16
        // initialCapacity == 17   threshold = 32
        newCap = oldThr;	//将计算的实际容量赋值为新的容量
    else {   
        // 使用HashMap的空参构造方法创建hashMap对象会走这里的逻辑
        // threshold=0时,会进入这里的逻辑
        
        // 将初始容量newCap = 16
        newCap = DEFAULT_INITIAL_CAPACITY;	
        // 计算新阈值newThr = (capacity * load factor)
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }

    // 如果newThr==0,根据新容量计算newThr
    if (newThr == 0) {
        // 计算新容量的阈值ft
        float ft = (float)newCap * loadFactor;
        // 新容量 < (1<<30) 并且 计算的阈值ft < (1<<30) ? (int)ft : Integer.MAX_VALUE
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    //将 newThr 赋值给 阈值
    threshold = newThr;
 //-----------------------------上面的操作就是确定新容量和新阈值--------------------------------
    // 下面则是进行扩容
    
    @SuppressWarnings({"rawtypes","unchecked"})
    // 根据新容量newCap,创建新的hash表
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    
    
    // 如果不是初始创建hashmap,而是对旧的hash表进行扩容,下面是扩容的逻辑:
    if (oldTab != null) {
        // 遍历旧的hash表,将旧的hash表的节点移到新的hash表中
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            // hash表的第j个桶有元素存在,将该元素保存在e,然后旧的hash表置为null
            if ((e = oldTab[j]) != null) {
            	// 取出oldTab[j]的元素赋值给e,然后清空旧hash表的元素
                oldTab[j] = null;
                // 如果hash表的第j个位置只有一个元素,则在新hash表中重新计算e元素的下标,并放入其中
                if (e.next == null)
                // 通过该元素保存的hash值 & (newCap - 1),这样可以将hash值映射在[0,newCap-1]范围内
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    // 如果e是一个树的节点,这个和链表移动差不多,也是将树中的节点分摊,然后放入新的tab,
                    // 将tree中的节点分为两部分,两个树,然后判断树的节点个数是否<=6,如果<=6则将树转为链表,否则依然为红黑树
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // 否则e就是链表的第一个节点
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    // 遍历链表,将e处位置的链表,移动到新的hash表
                    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);
                    // 将重新计算的链表放入新的hash表
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    
    return newTab;
}
get(Object key)方法
public V get(Object key) {
    Node<K,V> e;
    // 先计算key的hash值,然后调用getNode()方法
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    // 判断hash表不为null,且长度>0,且tab[i]的第一个元素不为null,才进行get操作,否则直接返回null
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        
        // 比较桶的第一个元素,如果相等,则找到,直接返回第一个元素
        if (first.hash == hash && 
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        
        // 如果第一个元素key不相等,如果该桶的第一个元素的下一个元素不为null,则进行遍历
        if ((e = first.next) != null) {
            // 如果是TreeNode,则调用getTreeNode遍历红黑树获取value
            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;
            } while ((e = e.next) != null);
        }
    }
    return null;
}
remove(Object key)方法
public V remove(Object key) {
    Node<K,V> e;
    // 移除某个元素,如果存在,则返回移除的元素的value,否则返回null
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}
/*
 * @param hash 根据key计算的hash值
 * @param key 移除的key
 * @param value 要匹配的value,没有则忽略
 * @param matchValue 如果为true,则仅当value相等时才移除
 * @param movable 如果为false,则在删除时不要移动其他节点
 * @return 返回移除的节点,或者null
*/
final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    // 仅当元素存在才移除,否则直接返回null
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        
        Node<K,V> node = null, e; K k; V v;
        
        // hash表的第一个元素相等,则记录该元素为node
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        // 如果第一个元素不相等,并且存在下一个元素
        else if ((e = p.next) != null) {
            // 如果是TreeNode节点
            if (p instanceof TreeNode)
                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
        // 如果hash表存在要移除的节点,则进行移除
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            // 如果node是TreeNode节点,进行移除
            if (node instanceof TreeNode)
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            // 如果node是某个桶的第一个元素,则将node的下一个元素作为该桶的第一个元素
            else if (node == p)
                tab[index] = node.next;
            // 如果node是某个桶链表的某个节点,则进行移除,p节点为node节点的前一个节点
            else
                p.next = node.next;
            // 修改次数+1
            ++modCount;
            // 元素-1
            --size;
            afterNodeRemoval(node);
            // 返回移除的节点
            return node;
        }
    }
    return null;
}
containsKey(Object key)方法
public boolean containsKey(Object key) {
    // 底层调用的是get()方法,判断get方法有没有返回值
    return getNode(hash(key), key) != null;
}
containsValue(Object value)方法
public boolean containsValue(Object value) {
    Node<K,V>[] tab; V v;
    // hash表存在
    if ((tab = table) != null && size > 0) {
        // 遍历hash表第一个元素
        for (int i = 0; i < tab.length; ++i) {
            // 从第一个元素开始遍历链表和红黑树
            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;
}
总结:

put总体流程:

  1. 计算key的hash值
  2. hash表没有初始化则进行初始化
  3. tab[i]==null,则new节点放入该位置
  4. tab[i]!=null
    1. 如果tab[i]的key跟插入的key相同,则替换值
    2. 如果tab[i]是一个红黑树节点,则插入红黑树中
    3. 如果tab[i]是一个链表节点,则插入链表中
  5. 如果链表 szie>=8 && tab.length >= 64则进行树化,否则进行扩容

resize总体流程:

  1. 计算新的hash表容量和新的阈值
  2. for i -> size:遍历oldTab,进行迁移
    1. 如果oldTab[i]只有一个元素,则直接重新hash然后赋值给newTab
    2. 如果oldTab[i]是一个红黑树节点,则进行拆分,高低位拆分然后分配到新hash表中,如果拆分的元素个数<=6,则将红黑树退化为链表
    3. 如果oldTab[i]是一个链表,则遍历链表,进行高低位拆分,分配到新的hash表中
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值