HashMap源码学习

1. HashMap参数含义及用途

  1. 装载因子。用于判断是否需要扩容。默认值0.75
java 复制代码
static final float DEFAULT_LOAD_FACTOR = 0.75f;
  1. 用于冲突key的树链转换。
java 复制代码
 static final int TREEIFY_THRESHOLD = 8;
 static final int UNTREEIFY_THRESHOLD = 6;
  1. 存储元素的。
java 复制代码
transient Node<K,V>[] table;

node的具体实现

java 复制代码
static class Node<K,V> implements Map.Entry<K,V> {
		// 哈希值
        final int hash;
        // key值
        final K key;
        // value值
        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;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }
  1. 扩缩容的阈值,装载因子。
c 复制代码
int threshold;

final float loadFactor;

2. 常用方法

  1. get方法
  • get方法如下
c 复制代码
public V get(Object key) {
	Node<K,V> e;
	return (e = getNode(hash(key), key)) == null ? null : e.value;
}

这段代码主要是通过getNode方法,获取node元素并赋值给e。如果获取不到就返回空;否则返回e的value。

  • getNode方法如下:
c 复制代码
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) {
            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;
    }
  • if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null)
    第一行判断首先将table赋值给tab,通过tab对table进行操作。然后判断map中有值,也就是table长度不为0。最后通过hash值与上n-1得到要找的元素在table中的位置,并赋值给first。
  • if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k))))
    第二行判断,判断当前位置的第一个Node的hash值是否等于要查找的元素的hash值,并且判断key是否相等。
    相等就返回第一个Node,即first。
  • 前面判断后没有返回当前Node说明存在key冲突的情况。HashMap解决冲突的办法是拉链法(链地址法),当元素过多时,为了优化查询效率,将链表转为红黑树。
    所以接下来分两步,首先获取当前节点e = first.next。如果他是树节点类型,就在树中进行查找。否则是链表,遍历链表查找是否存在目标节点。
  • 最后如果找不到就返回空。
  1. put方法
  • put方法如下:
java 复制代码
public V put(K key, V value) {
	return putVal(hash(key), key, value, false, true);
}

put方法计算key的hash值,和key,value一起作为参数调用putVal方法。

  • putVal方法如下:
java 复制代码
 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)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            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;
    }
  • if ((tab = table) == null || (n = tab.length) == 0) 第一行将table赋值给tab进行操作。判断tab是否为空,或者长度是否为0。
    如果为空就调用resize()方法初始化。
  • if ((p = tab[i = (n - 1) & hash]) == null)第二行通过(n - 1) & hash计算当前元素在table中的位置,并将对应位置的Node元素赋值给p。如果p为空,说明当前位置没有插入元素,new一个Node进行插入。
  • else走到这里说明要插入的位置已经存在元素了。
    if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
    首先判断第一个元素是否是要找的元素,通过判断key是否相等,以及hash值是否相等来判断。如果是就把当前的p节点赋值给e。
    else if (p instanceof TreeNode)
    如果第一个节点不是要查找的节点,说明存在冲突。然后判断是否是树节点,如果是树节点,执行树节点中的插入。并将插入的节点元素赋值给e。
    否则说明是链表。执行如下代码:
java 复制代码
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;
}
  • 最外层for循环,binCount用来对元素个数进行计数。 if ((e = p.next) == null) {判断是否走到的最后一个元素的后面一个位置,也就是空,需要插入的位置。newNode插入,插入后判断是否达到链表转为树的阈值。达到就转换为树。if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) 如果不是空,说明还在遍历链表节点。判断节点是否是要查找的节点。如果是就结束循环。因为上面的判断语句已经将节点元素赋值给了e,所以这里可以直接结束循环。
    否则继续遍历链表的下一个节点。
  • 根据上面的for循环,如果e为空,说明当前元素不在map中。执行的插入逻辑。如果e不为空,说明已经存在相同key的元素了,这个时候需要执行覆盖逻辑。
java 复制代码
if (e != null) { // existing mapping for key
   V oldValue = e.value;
    if (!onlyIfAbsent || oldValue == null)
        e.value = value;
    afterNodeAccess(e);
    return oldValue;
}
  • if (++size > threshold) resize();判断map中元素的数量是否大于了阈值,大于阈值需要进行扩容。
  1. resize()扩容逻辑
java 复制代码
final Node<K,V>[] resize() {
        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;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            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"})
        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)
                        ((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;
                            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;
    }

oldTab指向旧的table。oldCap和oldThr分别记录旧的table容量,和旧的阈值。newCap,newThr记录扩容后的参数信息。

  • 判断旧的table的容量是否大于0。大于0并且大于最大容量,不进行扩容。否则判断扩容后的容量是否小于最大容量并且旧的容量大于等于默认的初始化容量。如果是就进行扩容,二倍扩容。
java 复制代码
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
}
  • else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr;到这里说明table容量为0。进行table的初始化。

  • else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); }如果没有赋值阈值和容量,计算默认的阈值和容量。

  • 按照扩容后的容量创建新的表table。

java 复制代码
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)
                ((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;
                    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;
                }
            }
        }
    }
}

如果旧数组不为空,遍历旧的table。重新计算元素位置,插入到新的数组中。

分三种情况:

  • if (e.next == null)如果e的next为空,说明当前key的元素不存在冲突的情况,只有一个元素。这种情况计算元素在新数组中的位置。根据e.hash & (newCap - 1)计算新数组中的位置。
  • else if (e instanceof TreeNode)如果是树,那么进行树的拆分,或者树退化为链表。
  • 如果是链表,遍历链表元素,计算位置。
java 复制代码
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);
if (loTail != null) {
    loTail.next = null;
    newTab[j] = loHead;
}
if (hiTail != null) {
    hiTail.next = null;
    newTab[j + oldCap] = hiHead;
}

loHead,loTail记录在原位置的元素,hiHead,hiTail记录在新位置的元素
(e.hash & oldCap) == 0当前元素的hash与上旧数组的容量,为0在原位置。为1在新的位置。

相关推荐
炽烈小老头2 小时前
【 每天学习一点算法 2026/03/14】二叉搜索树中第K小的元素
学习·算法
别催小唐敲代码2 小时前
个人笔记网站搭建完整教程
笔记·学习·个人博客
妄汐霜2 小时前
小白学习笔记(ES6)
笔记·学习
升职佳兴3 小时前
Excel 学习笔记整理:常用操作、数据清洗与公式应用实战
笔记·学习·excel
悠哉悠哉愿意3 小时前
【单片机学习笔记】math库函数补充
c语言·笔记·单片机·学习
future02103 小时前
Kafka再平衡:从救火到优雅控场
学习·kafka
tritone3 小时前
最近在学习网络配置中的Port Forwarding(端口转发)技术,为了有个稳定的实验环境,我试用了阿贝云的免费虚拟主机和免费云服务器
服务器·网络·学习
for_ever_love__3 小时前
Objective-C学习 类别和扩展
学习·算法·objective-c
历程里程碑3 小时前
36 Linux线程池实战:日志与策略模式解析
开发语言·数据结构·数据库·c++·算法·leetcode·哈希算法