Java:HashMap(JDK1.8)

180 阅读4分钟

「这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战

在本篇主要整理一下 1.8 的 HashMap 进行分析,主要从以下方面:

  • 存储结构

  • 扩容机制

基本属性

下面列出 HashMap 中的属性值并加以节是

// 部分常量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;	// 初始大小 16
static final int MAXIMUM_CAPACITY = 1 << 30; 		// 最大容量
static final float DEFAULT_LOAD_FACTOR = 0.75f;		// 负载因子,当 size 超过负载因子与当前数量的乘积时会再添加节点会进行扩容
static final int TREEIFY_THRESHOLD = 8;				// 链表大小大于该值时转为红黑树

// 属性
transient Node<K,V>[] table;	// HashMap 的基本结构:数组
transient int size; 			// 当前数量
int threshold;					// 当前容量阈值(size * threshold)
final float loadFactor;			// 负载因子,final 修饰,确定后不可修改

构造函数

// 可以对 HashMap 的 初始容量 及 负载因子 进行指定
public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    // 初始容量超过最大值会被重设
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    // 下面的 tableSizeFor 主要将容量调整为 2 的幂
    this.threshold = tableSizeFor(initialCapacity);
}
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;
}

存储结构

在了解 HashMap 的整体结构前,先来看看其节点构成

Node

Node 继承自 Map.Entry,是一种键值存储结构。

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;		//若 hash 值相同,则会对比 key 确定是否为同一对象
    final K key;	// 键值
    V value;		// 值
    Node<K,V> next;	// 下一指针,用于构造链表解决 hash 冲突问题
    // ...
}

hash 操作

取 key.hashCode() 进行 hash 操作,因此重写 equals方法时,要对hashcode进行重写,不然可能导致equals相同,hashcode不同的结果(hashcode 默认是对象存储的内存地址,对具有相同属性值的对象也会判定为不相等)。

static final int hash(Object key) {
    int h;
    // >>> 无符号右移:
    // 0000 0100 1011 0011  1101 1111 1110 0001 >>>(16) 0000 0000 0000 0000  0000 0100 1011 0011
    // 令低位掺杂高位特征
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

get 操作

获取操作时 (n - 1) & hash替代对数组长度的取余操作,提高计算速率。

public V get(Object key) {
    Node<K,V> e;
    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;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        // 查看对应位置的第一个节点,同时需要使用 equals 比较
        if (first.hash == hash && // always check first node
            ((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;
}

put 操作

// 传入时对 key 进行 hash,放入对应的位置
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

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)
        // resize 对 hashmap 数组进行初始化或者扩容,此时为扩容
        n = (tab = resize()).length;
    // 要放置的数组的对应位置没有元素,则直接插入即可
    // (n - 1) & hash 防止长度溢出,使用 & 运算取代 % 运算,提高效率
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        // key 相同则替换值
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 若该 node 为 TreeNode,对其进行树相关操作
        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) {
                    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;
                p = e;
            }
        }
        // 获取到的值不为空
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    // 扩容后的容量增加,以及判断是否超过负载,从而重新排布
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

扩容机制

resize

用于初始化或扩容,每次扩容都为 2 的幂,扩容完成后原来的元素会保持相同的索引或者较原来有 2 的幂的量的偏移。

重新分布时,用(e.hash & oldCap) == 0作为区分,比起 JDK7 重新计算 hash,得到效率上的提升。

final Node<K,V>[] resize() {
    // 获取旧表 table
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    // 旧表进行扩容
    if (oldCap > 0) {
        // 大于最大容量无法再进行调整,直接返回
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 扩容,并调整 threshold 大小
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // threshold 翻倍
    }
    // 带参数的初始化
    else if (oldThr > 0) // 初始容量被设为 threshold
        newCap = oldThr;
    // 不带参数的初始化
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    
    // 计算新的 threshold
    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) {
        // 遍历数组
        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)
                    // split 方法对树进行拆分,若拆分后节点数量太少会取消树化
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                // 链表情况
                else { // preserve order
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        // 通过该方式进行重新散列
                        // 使用 (e.hash & oldCap) 区分索引位置
                        // 等于0则为原位置,否则位置为当前偏移旧数组长度
                        // lo:记录索引不变的节点
                        // hi:记录索引偏移的节点
                        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;
}