我的HashMap源码阅读记录

认识HashMap

HashMap是平时开发中常用一种数据结构。其底层精妙的设计大大提高了我们开发出来的产品的底层性能。那么它是如何实现的,我们来一探究竟

底层数据结构

JDK1.7及之前 桶数组(bucket) + 链表

JDK1.8之后 当链表达到一定长度时 会转化为红黑树

其实就是哈希散列法中的“拉链法”,只不过当一条链长度过长时,为了增加查询数据的性能,在JDK1.8之后就会将链条树化(红黑树)

image-20220731133023452

构造函数

// 默认构造函数 加载因子为默认值0.75f
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
}

// 包含另一个“Map”的构造函数
public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

// 指定了容器大小的构造函数
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}


// 指定“容量大小”和“加载因子”的构造函数
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;
    this.threshold = tableSizeFor(initialCapacity);
}

一般常用的就是构建一个空的HashMap,别的构造函数涉及到了一些需要知道的概念,借此机会一次性说掉

门限值

门限值Threshold就是扩容机制触发的一个阈值 计算公式为:

门限值 = 数组容量 X 负载因子

其中负载因子会在下面做出说明

初始化容量

指的就是桶数组(bucket)的初始化大小 默认为16

为什么是16呢 简单的解释就是使用2的次方的容量可以在HASH取模运算后使得散列更加均匀 学过数据结构的朋友应该都有印象 书上建议初始化大小为2的次幂 在这里JDK开发者选择了16作为默认的值

这些说明了为什么扩容时都是乘以2 就是为了保证桶数组的容量为2的次幂

在构造函数中若指定了一个初始的大小 则初始容量会进行计算 找到一个最近的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;
}

乍看是不是很懵逼 这个算法十分精妙 来理解一下

比如要求17对应的2次幂数 易得出是32

那么这个算法如何实现的呢 就是先将17 -1 得到 16 二进制就是 10000

只要将二进制数第一个为1的位置开始,后面的数字全变为1 最后再加上1 就是我们要的2次幂数了 10000 -> 11111(31) -> 31 + 1 -> 32

这个算法就是实现将后面数全变为1的操作 那为什么是逻辑右移1,2,4,8,16 就结束了呢

这样思考 一个位置为1 右移1位 就得到了两个1 此时右移2位 就得到了4各个1 以此类推 当右移16位时 刚好一个int类型位数操作完 自然可以得到任何int类型数据的所要求的二次幂数

负载因子

负载因子可以自定义,默认为0.75f

该参数用于调节散列效果 例如当负载因子为1时 数组需要到达容量大小则才开始扩容 不可避免会出现很多HASH冲突 导致某个节点的链条过长 影响查询性能 当负载因子为0.5时 每当数组容量使用了一半时就开始扩容了 虽然会大大降低HASH冲突的概率 但散列效果却会不尽如人意 于是取中间值0.75作为默认的负载因子 开发者可以根据需求 自由调节

扰动函数

HashMap中有这样一个方法专门用于求键值的HASH值

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

可见,在key求hashcode之后又将其与自己逻辑右移16位后的结果做了按位与操作,那么有两个问题

  • 为什么要做按位与
    • 保留hashcode的结果特征 增强散列效果
  • 为什么是16位
    • 因为hashcode表示数据范围是int类型数据范围 众所周知 int由32个bit组成 因此逻辑右移16位(一半距离) 以此结合一个hashcode高位和低位的特征 以降低在对桶数组容量取模运算后的hash冲突

image-20220731135448439

个人理解:这些参数的存在,就是为了数据有一个较好的散列效果,以增强数据查询插入的效率,以达到高性能的效果

HashMap基本操作和遍历

先循环插入一些数

HashMap<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < 14; i++) {
    map.put(i, (int) (Math.random() * 100));
}
System.out.println("直接输出HashMap");
System.out.println(map);
直接输出HashMap
{0=43, 1=14, 2=54, 3=22, 4=75, 5=36, 6=73, 7=37, 8=46, 9=77, 10=69, 11=23, 12=63, 13=85}

这里可见HashMap以键值对的形式返回了数据

4种遍历方式

// ====ForEach循环遍历====
System.out.println("Iterate entries using ForEach loop:");
for (Map.Entry<Integer, Integer> stringStringEntry : map.entrySet()) {
    System.out.println("Key = " + stringStringEntry.getKey() + ", Value = " + stringStringEntry.getValue());
}


// ====ForEach迭代键值对方式====
System.out.println("Iterate keys / values using ForEach loop:");
for (Integer key : map.keySet()) {
    System.out.println("Key=" + key);
}
for (Integer value : map.values()) {
    System.out.println("Value=" + value);
}


// ====迭代器遍历===
System.out.println("Iterate using Iterator with generics=v:");
Iterator<Map.Entry<Integer, Integer>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
    Map.Entry<Integer, Integer> entry = iterator.next();
    System.out.println("Key=" + entry.getKey() + " Value=" + entry.getValue());
}


// ====Java Lambda表达式遍历====
System.out.println("Iterate using Java 8 Lambda Expression:");
map.forEach((K, V) -> System.out.println("key: " + K + " value:" + V));

打印结果太多 就不展示了 可以自己试一下 个人觉得Lambda表达式简直爱了好吗 Elegant And Concise 优雅且简洁

底层数据结构的构建

下面就来看看所谓的桶数组+链表+红黑树到底是怎么构建 让我们一起撕开神秘的面纱

节点类

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;
    }

    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;
    }
}

这便是HashMap中最基本的一个节点的构成 它实现了Map.Entry<K,V> Entry是Map的一个静态内部类,用于存放键值对

值得注意的是,这里Node的equals方法,是比较Key和Value的equals方法,在开发中,如果有需求要进行个性化的比较,就需要重写我们自己的Object类的equals方法了

插入节点的putVal()方法

此方法是一个内部方法,仅在内部调用。在我们插入一个节点时,就会调用该方法

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

putVal方法也详细的展示了怎么构建这么一个精妙的数据结构的

先上源码 前方高能~~~

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;
}

头疼的来了 看看怎么拆解 怎么理解

做好预备工作

image-20220731145208320

这里进行一系列的初始化 tab即是桶数组容器 p就是插入的单个节点 若一上来table为null 则说明HashMap仅仅是做了初始化 还没有一个初始化的大小空间 那么就resize()方法分配初始空间 这里只要知道resize方法就是用来分配空间的 具体细节后面会做分析

image-20220731145500014

第一种情况(当前位置为null)

HASH分配到的偏移位置还没有插入节点,那么二话不说,直接塞入新节点

if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);

值得注意的是 数组偏移是通过 (n - 1) & hash 得到的 其实 这就是将hash对数组容量做了一个取默运算 只不过数组偏移是从0开始的 所以是n-1 非常精妙

第二种情况(hash分配的位置相同且Key相同)

说明这是节点更新,直接将当前节点更新

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);
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
                               int h, K k, V v) {
    Class<?> kc = null;
    boolean searched = false;
    TreeNode<K,V> root = (parent != null) ? root() : this;
    for (TreeNode<K,V> p = root;;) {
        int dir, ph; K pk;
        if ((ph = p.hash) > h)
            dir = -1;
        else if (ph < h)
            dir = 1;
        else if ((pk = p.key) == k || (k != null && k.equals(pk)))
            return p;
        else if ((kc == null &&
                  (kc = comparableClassFor(k)) == null) ||
                 (dir = compareComparables(kc, k, pk)) == 0) {
            if (!searched) {
                TreeNode<K,V> q, ch;
                searched = true;
                if (((ch = p.left) != null &&
                     (q = ch.find(h, k, kc)) != null) ||
                    ((ch = p.right) != null &&
                     (q = ch.find(h, k, kc)) != null))
                    return q;
            }
            dir = tieBreakOrder(k, pk);
        }

        TreeNode<K,V> xp = p;
        if ((p = (dir <= 0) ? p.left : p.right) == null) {
            Node<K,V> xpn = xp.next;
            TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
            if (dir <= 0)
                xp.left = x;
            else
                xp.right = x;
            xp.next = x;
            x.parent = x.prev = xp;
            if (xpn != null)
                ((TreeNode<K,V>)xpn).prev = x;
            moveRootToFront(tab, balanceInsertion(root, x));
            return null;
        }
    }
}

具体怎么加入的,需要先了解红黑树,这里不做多说明

第四种情况(加入链表节点)
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;
    }
}

遍历链表,若hash值Key值均一样,则直接退出循环

若链表长度即将大于树化的门槛值,则要靠考虑进行树化

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)
        resize();
    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);
    }
}

首先会判断桶数组容量是否达到可以树化的最低标准,没有则先进行桶数组的扩容再HASH,达到了则将链表转化为红黑树

HashMap的resize()方法

在HashMap中,采用二次方扩容法

当HashMap在元素达到负载因子阈值时,便会自动扩容,以保证HASH得到很好的散列

Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;

resize()方法的一开始,进行一系列的初始化。下面分种情况来讨论

无参构造的空HashMap

由于是一个空的HashMap,oldTable oldCap oldThr均为空

image-20220819114543226

于是直接初始化容量为16,门限值为0.75*16 = 12

达到门限值需要扩容

image-20220819135919766

  • 如果原来的桶数组大小已经超过最大限定容量(1 << 30)了,那么将门限值设定为Integer数据类型的最大值(1 << 31)
  • 否则,就直接按照2次幂扩展规则,将桶数组容量和门限值都扩大为原先的2倍

接着,不可避免的,需要rehash

大概结构如下:遍历每个节点,进行调整

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;
            // 一系列操作
           	......
        }
    }
}
数组的调整
if ((e = oldTab[j]) != null) {
    oldTab[j] = null;  // 这一步将原数组对应位置的节点设为NULL
    if (e.next == null)  // 如果该节点的下一个为空,则说明此处既不是链表,也不是红黑树,直接进行哈希再分配
        newTab[e.hash & (newCap - 1)] = e;
  	......
    }
}
链表的调整
if ((e = oldTab[j]) != null) {
    oldTab[j] = null;
    // 单个节点
    ......
    // 红黑树
    ......
    // 链表
    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;
        }
    }
}

乍一看很复杂,是的。

首先,要明白高低位链表是什么:

将每个节点HASH值与oldCap按位与,如果为0,则加入低位链表,如果高位为1,则加入高位链表

为什么会这样呢,举个例子,oldCap为16

若7(00111) & 16(10000) => 00000 则加入低位链表

若17(10001) & 16(10000) => 10000 则加入高位链表

下面看看怎么通过循环构建两根链表

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);

loHead loTail分别指向低位链表的头尾节点

hiHead hiTail分别指向高位链表的头尾节点

接着将两条链表加入新的桶数组中

if (loTail != null) {
    loTail.next = null;
    newTab[j] = loHead;
}
if (hiTail != null) {
    hiTail.next = null;
    newTab[j + oldCap] = hiHead;
}

最后,借互联网上的一张图来帮助理解

image-20220819144720573

红黑树的调整
if ((e = oldTab[j]) != null) {
    oldTab[j] = null;
    // 单节点
    ......
    // 红黑树
    else if (e instanceof TreeNode)
        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
   // 链表
    ......
}

其中split函数细节为:

final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
    TreeNode<K,V> b = this;
    // Relink into lo and hi lists, preserving order
    TreeNode<K,V> loHead = null, loTail = null;
    TreeNode<K,V> hiHead = null, hiTail = null;
    int lc = 0, hc = 0;
    for (TreeNode<K,V> e = b, next; e != null; e = next) {
        next = (TreeNode<K,V>)e.next;
        e.next = null;
        if ((e.hash & bit) == 0) {
            if ((e.prev = loTail) == null)
                loHead = e;
            else
                loTail.next = e;
            loTail = e;
            ++lc;
        }
        else {
            if ((e.prev = hiTail) == null)
                hiHead = e;
            else
                hiTail.next = e;
            hiTail = e;
            ++hc;
        }
    }

    if (loHead != null) {
        if (lc <= UNTREEIFY_THRESHOLD)
            tab[index] = loHead.untreeify(map);
        else {
            tab[index] = loHead;
            if (hiHead != null) // (else is already treeified)
                loHead.treeify(tab);
        }
    }
    if (hiHead != null) {
        if (hc <= UNTREEIFY_THRESHOLD)
            tab[index + bit] = hiHead.untreeify(map);
        else {
            tab[index + bit] = hiHead;
            if (loHead != null)
                hiHead.treeify(tab);
        }
    }
}

看起来有些复杂,其实和链表一样的。根据HASH将红黑树分出高低位链表来。再将两条链表树化塞入新的桶数组中。其中,有很多细节和特殊情况,暂不做讨论。关于红黑树的理论和实现,这里也不再详细赘述。

这样,对于HashMap是如何进行扩容的,我们有了一个基本的认识

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Bruce_Zhang61

给萌新一点鼓励吧!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值