【集合】HashMap源码及设计分析

本文详细探讨了Java HashMap的数据结构和设计理念,包括其在JDK1.8后的优化,如数组+链表或红黑树的存储方式。分析了HashMap的内部属性、哈希函数、构造函数、get、put、resize和remove等关键方法的实现。此外,还介绍了扩容策略和数据迁移的细节,以及在特定情况下如何避免树化并进行隐式扩容。通过对源码的解析,有助于开发者更深入地理解和应用HashMap。
摘要由CSDN通过智能技术生成

前言

HashMap作为一种键值对(KEY-VALUE)的数据结构,其设计思想,在日常开发中有着举足轻重的地位。作为Java开发的进阶学习,除了日常的使用之外,也应该对底层设计原理及源码有一些研究,便于开发过程中更好地选择数据结构、平衡性能、评估并发风险等。

存储结构

JDK1.8之后,为了提高Hash冲突的查询效率、解决循环依赖等问题,HashMap底层做了一些优化,其内部存储结构使用的是数组+链表或红黑树,如下图所示。

图1. HashMap存储结构(JDK1.8)

绿色节点的是KEY的HashCode值在数组中的位置(经过取模运算)。
红色节点有两种类型,针对链表结构,是实现Map.Entry<K,V>接口的Node节点;

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

而对于红黑树结构,则是实现了LinkedHashMap.Entry<K,V>接口的TreeNode节点。

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;
        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
        }
}

源码解析

内部属性

1、静态常量

属性名属性值备注
DEFAULT_INITIAL_CAPACITY16默认容量大小
MAXIMUM_CAPACITY1<<30最大容量大小
DEFAULT_LOAD_FACTOR0.75默认负载因子,负载因子*容量,作为扩容的临界条件
TREEIFY_THRESHOLD8节点树化临界值,当节点数达到最小树化容量,且数组中节点个数超过此值,会进行树化
UNTREEIFY_THRESHOLD6树化的节点数低于此值,会转成链表
MIN_TREEIFY_CAPACITY64最小树化容量,当size低于此值,及时节点数不低于TREEIFY_THRESHOLD,也不会进行树化操作

2、私有属性

属性名类型备注
tableNode<K,V>[]节点数组,HashMap的底层存储结构
entrySetSet<Map.Entry<K,V>>遍历辅助set集
sizeint当前Map中有效键值对数量
modCountint当前Map修改次数,用于iterator,迭代中间modCount编发,会抛出ConcurrntModificationException异常
thresholdint扩容的阈值
loadFactorfloat负载因子

重点方法剖析

0、HASH(KEY)

哈希函数比较简单,获取key的hashCode,并与其高16位取异或运算。如此设计的目的,是为了增加hash值的复杂度,降低hash冲突的概率。

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

1、CONSTRUCTOR

常用构造函数,做了参数的临界值判断之外,仅对loadFactor和threshold赋值,并未对table分配空间,这里使用了懒加载的思想,在put时分配内存空间,避免空间浪费。

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

tableSizeFor是HaspMap的内部静态函数,主要作用是获取进来的参数转变为2的n次方的数值。

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;
    }
:
cap = 10;      // 00000000 00000000 00000000 00001010
n = 9;         // n = 00000000 00000000 00000000 00000101
n |= n >>> 1;  // n = 00000000 00000000 00000000 00000111
后面右移取或,最终tableSizeFor的大小为16

2、GET(KEY)

在这里插入图片描述

3、PUT(KEY, VALUE)

在这里插入图片描述
这里可以看出,put函数的流程中,有两处显示的扩容调用。但在树化的过程中,如果当前size小于MIN_TREEIFY_CAPACITY,此时并不会进行树化操作,而是进行一次隐式扩容。具体用途如下:

1. 第一次扩容,是懒加载的设计思想,避免内存浪费
2. 第二次扩容,针对新插入的键值对,size自增后超过扩容阈值,发起的扩容操作
3. 第三次扩容(树化),当size小于MIN_TREEIFY_CAPACITY,此时会扩容来避免一次树化

4、RESIZE()

如前所属,HashMap扩容的几个时间点,容量是按照2倍去递增,基本流程如下:

  1. 扩容临界条件判断,是否支持扩容。计算新的扩容阈值、分配扩容后数组等
  2. 旧数组的节点数据迁移

resize方案流程图

数据迁移有几种情况(oldCap: 旧数组容量,newCap:新数组容量):

  • i 位置的单个节点的迁移,位置变为【e.hash & (newCap - 1)】,保持不动,要么顺移至 (i + oldCap) 位置
  • j 位置的链表迁移,按照【(e.hash & oldCap) == 0】条件拆分列表,条件为真,保持 j 位置不动,否则,移动至 (j + oldCap),保证链表节点之间的顺序性
  • k 位置的红黑树迁移,类似链表迁移流程,先分割,再根据条件迁移

核心迁移代码实例如下:

for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;     // 手动赋值为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;
                        }
                    }
                }
            }

这里手动将oldTab[j]赋值为null,借助jvm的垃圾回收机制,自动回收旧数组占用的内存

5、REMOVE(KEY)

在这里插入图片描述
remove函数,需要判断当前的KEY是否在集合中,因此会有get函数流程。获取到目标节点后,根据节点类型,删除节点,并返回删除节点的VALUE值
这里需要注意,如果删除的是TreeNode节点,红黑树平衡会有自旋平衡,同时可能会退化成链表结构

知识有限,难免不足,诚恳接受大家的批评指正!
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值