【学习笔记】Java笔记11:HashMap

162 阅读8分钟

本文介绍的内容不包含红黑树的操作

1. 介绍

  • 底层是数组 + 链表(JDK 1.8后是红黑树,当链表长度大于8时,链表转换为红黑树)

2. 源码

2.1 常量

  • 默认容量大小 DEFAULT_INITIAL_CAPACITY = 16

  • 数组最大长度 MAXIMUM_CAPACITY = 2^30

  • 默认负载因子 DEFAULT_LOAD_FACTOR = 0.75

  • 树化(某一条链表升级为红黑树)的阈值 TREEIFY_THRESHOLD = 8

    前提是所有元素数量达到 MIN_TREEIFY_CAPACITY = 64 后

  • 树降级称为链表的阈值 UNTREEIFY_THRESHOLD = 6

2.2 属性

// 哈希表
transient Node<K,V>[] table;

transient Set<Map.Entry<K,V>> entrySet;

// 哈希表的元素个数
transient int size;

// 哈希表的结构修改次数,如插入、删除(替换不算)
transient int modCount;

// 扩容阈值,当哈希表的元素超过阈值时,触发扩容
// 初始化为一个大于等于cap的值,且该值为2的幂(cap是调构造方法时的传参)
int threshold;

// 负载因子,threshold = capacity(当前数组的大小) * loadFactor
final float loadFactor;

2.3 构造方法

  • 构造方法里调用了int tableSizeFor(int cap)方法
// 构造方法传进来的容量大小可以是任意数,但该方法保证了初始化的大小为2的幂
// 返回一个大于等于cap的值,且该值为2的幂 
static final int tableSizeFor(int cap) {
    int n = cap - 1;
    // >>:带符号右移。正数右移高位补0,负数右移高位补1
    // >>>:无符号右移。无论是正数还是负数,高位通通补0
    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;
}

2.4 put方法

  • put之前先调用hash()方法计算key的哈希值,然后再进行路由寻址:(数组长度 - 1) & 哈希值
static final int hash(Object key) {
    int h;
    // key的哈希值 异或 它的高16位
    // 当数组的长度很短时,只有低位数的哈希值能参与运算;而让高16位参与运算可以更好的均匀散列,减少碰撞,进一步降低hash冲突的几率,并且使得高16位和低16位的信息都被保留了。
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
  • put方法里会调用putVal方法
/**
 * Implements Map.put and related methods.
 *
 * @param hash         key的哈希值
 * @param key          key
 * @param value        value
 * @param onlyIfAbsent 如果为true,则不替换key相同的value(put方法调用时传入fasle,则会替换)
 * @param evict        if false, the table is in creation mode.
 * @return previous value, or null if none
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    // 当前Map的散列表
    HashMap.Node<K, V>[] tab;
    // 当前元素
    HashMap.Node<K, V> p;
    // n:散列表的长度
    // i:路由寻址结果的下标
    int n, i;
    
    // 第一次调用putVal时才初始化Map中最耗内存的散列表
    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 {
        // 临时元素,最终会指向key相同的旧元素
        HashMap.Node<K, V> e;
        // 临时key
        K k;
        // 此时p指向该位置上的旧元素(链表的头节点 或 红黑树的根节点)
        // 如果该位置上的旧元素与新元素的哈希值相同,并且两者的key相同,则把e指向旧元素
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 如果旧元素已经形成红黑树
        else if (p instanceof HashMap.TreeNode)
            e = ((HashMap.TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
        // 此时链表的头节点不等于新元素,则需要逐个比较链表上是否有key相同的旧元素,没有则在末尾添加
        else {
            for (int binCount = 0; ; ++binCount) {
                // 遍历到最后一个元素,也没有key相同的元素,则在链表末尾添加新元素
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 如果当前链表长度超过树化阈值,则转成红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1)
                        treeifyBin(tab, hash);
                    break;
                }
                // 如果找到key相同的元素,跳出循环
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        // 此时e指向key相同的旧元素
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    // 散列表修改结构的次数加一
    ++modCount;
    // 插入新元素,size自增,如果大于扩容阈值,则触发扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

2.5 resize扩容方法

  • 为什么需要扩容:为了解决哈希冲突导致的链化影响查询效率的问题,扩容可以缓解该问题
final Node<K,V>[] resize() {
    // 扩容前旧的哈希表
    Node<K,V>[] oldTab = table;
    // 扩容前旧的table数组的长度
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    // 扩容前旧的扩容阈值
    int oldThr = threshold;
    // newCap:扩容后table数组的大小
    // newThr:扩容后,下次再触发扩容的阈值
    int newCap, newThr = 0;
    
    // 说明散列表已经初始化过了,这是一种正常扩容
    if (oldCap > 0) {
        // 达到这个条件就没法再继续扩容了,把下次扩容阈值调到最大
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 如果扩容一倍后仍然小于最大容量,并且旧的容量大于默认大小,则下次扩容阈值翻倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    // 此时oldCap = 0,说明还未初始化
    // 1. new HashMap(initCap, loadFactor);
    // 2. new HashMap(initCap);
    // 3. new HashMap(tempMap); 且tempMap有内容
    // 这三种情况把旧扩容阈值赋给新容量,newCap一定是2的幂
    else if (oldThr > 0)
        newCap = oldThr;
    // 此时oldCap = 0, oldThr = 0
    // new HashsMap();
    else {
        // 此时新容量为16
        newCap = DEFAULT_INITIAL_CAPACITY;
        // 新扩容阈值为 0.75 * 16 = 12
        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) {
        // j:扩容前旧的寻址地址
        for (int j = 0; j < oldCap; ++j) {
            // 当前node节点元素
            Node<K,V> e;
            // 当前桶位有数据,但是无法区分是单节点,还是链表,或者还是红黑树
            if ((e = oldTab[j]) != null) {
                // 置为null让GC回收
                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);
                // 上述两种情况不满足,此时就是链表,见章节2.5.1
                else {
                    // 低位链表:扩容前与扩容后的寻址地址一样的
                    Node<K,V> loHead = null, loTail = null;
                    // 高位链表:扩容后的寻址地址 = 扩容前的寻址地址 + 扩容前数组长度
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        // 下面的if/else的操作是把元素放到 低位链表 或 高位链表 中
                        // (e.hash & oldCap) == 0 说明这些元素应放到低位链表中
                        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);
                    // 扩容后,低位链表仍然放到旧的寻址地址j里
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    // 扩容后,高位链表放到新的寻址地址里
                    // 新的寻址地址 = 旧的寻址地址 + 扩容前数组长度
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

2.5.1 扩容时链表的处理

033-HashMap扩容之链表.png

2.6 get方法

  • get方法里会调用getNode方法
/**
 * Implements Map.get and related methods.
 *
 * @param hash key的哈希值
 * @param key  key
 * @return	   返回key-value节点,若无则返回null
 */
final Node<K,V> getNode(int hash, Object key) {
    // 散列表数组
    Node<K,V>[] tab;
    // first:桶位头元素
    // e:临时node节点元素
    Node<K,V> first, e;
    // 数组长度
    int n; 
    K k;
    // 数组不为空,并且寻址到的桶位不为空
    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;

        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;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

2.7 remove方法

  • remove方法里会调用removeNode方法
/**
 * Implements Map.remove and related methods.
 *
 * @param hash        key的哈希值
 * @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节点元素
    Node<K,V> p;
    // n:散列表数组长度
    // index:寻址结果
    int n, index;
    
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        // 执行到这说明当前桶位有数据,需要进行查找操作并删除
        // node:待删除元素
        // e:当前元素p的下一个元素
        Node<K,V> node = null, e;
        K k;
        V v;
        // (情况一)如果桶位第一个元素就是待删除元素,直接赋给node
        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)
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else {
                // (情况三)否则遍历链表,找到待删元素,赋给node
                // 结束循环后,p是node的前一个节点
                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,在matchValue为true的时候考虑value是否相等
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            // 对应情况二,如果是红黑树,走红黑树的删除逻辑
            if (node instanceof TreeNode)
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            // 对应情况一,直接把node.next放在桶位头节点
            else if (node == p)
                tab[index] = node.next;
            // 对应情况三,此时p是node的前一个节点,所以直接进行以下操作
            else
                p.next = node.next;
            // 哈希表的结构修改次数加一,哈希表大小减一
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

2.8 replace方法

/**
 * 获取待替换的元素,返回旧的value
 */
public V replace(K key, V value) {
    Node<K,V> e;
    if ((e = getNode(hash(key), key)) != null) {
        V oldValue = e.value;
        e.value = value;
        afterNodeAccess(e);
        return oldValue;
    }
    return null;
}

3. 相关链接