HashMap的源码分析

本文详细解析了HashMap在不同版本(JDK1.7和1.8)中的底层结构变化,重点介绍了数组、链表和从1.8开始引入的红黑树。核心内容包括核心常量和变量的解释,以及构造方法和put方法的实现,特别关注了扩容策略和哈希冲突处理。
摘要由CSDN通过智能技术生成

底层结构:

hashMap的底层结构主要包括数组和链表,以及从jdk1.8开始引入红黑树。具体来说:

  • JDK 1.7及之前:HashMap的基础结构是数组和链表,采用头插法进行元素的添加和删除操作。
  • JDK 1.8及以后:由于数组和链表在某些情况下(如大量元素)可能效率较低,因此从JDK 1.8开始,HashMap引入了红黑树作为其底层结构的补充。当数组长度超过64字节且链表长度大于8时,链表会被转换为红黑树以提高查找效率。

核心内容(常量变量)

核心常量释义:

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;
  1. DEFAULT_INITIAL_CAPACITY:默认初始容量
  2. MAXIMUM_CAPACITY:table最大长度
  3. DEFAULT_LOAD_FACTOR:加载因子
  4. TREEIFY_THRESHOLD:树化阈值
  5. UNTREEIFY_THRESHOLD:树降级链表阈值
  6. MIN_TREEIFY_CAPACITY:树化的另一个参数,数组阈值

核心变量释义:

transient Node<K,V>[] table;
transient int size;
transient int modCount;
int threshold;
final float loadFactor;
  1. table:哈希表 (一个Node<K,V>类型的数组)
  2. size:当前哈希表中的元素个数
  3. modCount:当前哈希表结构修改次数(替换不算)
  4. threshold:扩容阈值(当哈希表里的元素超过阈值时,会触发扩容方法)
  5. loadFactor:加载因子

构造方法分析

构造方法1.

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

通过传入指定的table长度与指定的加载因子创建HashMap

 前三个if为参数校验,capacity必须大于0,最大也就是常量MAXIMUM_CAPACITY(1<<30)

        this.loadFactor = loadFactor,即将指定的加载因子参数赋值给loadFactor

        this.threshold = tableSizeFor(iniitialCapacity)调用tableSizeFor方法将构造方法指定的通过计算得到2的幂次数的值赋值给threshold(table初始化的长度要求用2的幂次数)

tableSizeFor方法分析

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

作用:返回一个大于等于当前capacity值的一个数组,且这个数字一定是一个2的幂次数 

该算法让最高位的1后面的位全变为1。最后再让结果n+1,即得到了2的整数次幂的值了。

cap-1再赋值给n的目的是另找到的目标值大于或等于原值。例如二进制1000,十进制数值为8。如果不对它减1而直接操作,将得到答案10000,即16。显然不是结果。减1后二进制为111,再进行操作则会得到原来的数值1000,即8。通过一系列位运算提高效率。

构造方法2.

public HashMap(int initialCapacity) {
   this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

通过传入指定table长度和默认的加载因子(0.75f)调用构造方法1. 

构造方法3.

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

无参构造,将加载因子设置为默认常量值0.75f

put方法分析

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

 调用了hash方法,扰动hashCode值

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

扰动函数hash()的作用:让key的hash值的高16位也参与路由运算

key如果为null,那么hash值为0(由于哈希值决定着元素在table数组中的位置,如果key是null的话,它会位于table的首位)

最终通过将key的hashCode值与右移十六位后的hashCode值进行异或运算扰动后得到的int类型值返回

至此,我们可以看到put方法的内部实际上是调用了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;
    }

逐个分析 

Node<K,V>[] tab; Node<K,V> p; int n, i;

tab:引用当前hashMap的散列表

p:表示当前散列表的元素

n:表示散列表数组的长度

i:表示寻址结果

  if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;

延迟初始化逻辑,第一次调用putVal时会初始化hashMap对象中的table 

(tab = table) ==  null   将table赋值给tab,如果是null的话,说明hashMap里的table没有初始化,tab.length = 0 也是说明table没有初始化(在我们使用无参创建hashMap时,table就是没有初始化的)

n = (tab = resize() . length) 在第一次往hashMap中插入元素时,便会调用resize方法并初始化table

情况1.

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

路由算法,假如目前的表长度为16,那么将16-1再&哈希值便会找到一个该值在table数组中要存放的位置

比如说计算出的值为 5 ,那么我们就会先判断tab[ 5 ] 的位置是否为null,如果为null那么就会直接添加进tab中

情况2.

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

         Node<K,V> e; K k;

        e:不为null的话,找到一个与当前要插入的key一致的元素

        k:一个临时的key

这时,要添加的tab数组的这个位置不为null,也就是已经有元素存在了

情况2.1

if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;

p.hash ==  hash  当前tab中的该位置的元素哈希值与要插入元素的key的哈希值相同,

并且,key做==判断和equals判断也为true,即说明要插入的元素与tab中的该元素是完全一致的,那么后续会进行一个替换操作(位于末尾的代码)

            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }

情况2.2

 else if (p instanceof TreeNode)
             e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

如果他们是不相同的元素,则会判断该位置是否已经树化为红黑树,如果已经树化了,则执行putTreeVal方法在红黑树中添加该元素

情况2.3

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

大致意思:此时,tab数组要插入的位置不为null,则在该位置的链表上开始比较,如果在链表上有元素是和要插入的元素相同的,则会发生替换操作,否则说明要插入的元素在整个hashMap中是唯一的,那么就会将它添加在链表的末尾处

for循环用于遍历这个链表,

情况2.3.1

        if ((e = p.next) == null) 当这条判断语句为true时,说明已经遍历到了链表的尾部,并且没能在链表上找到与之相同的元素,则将要插入的元素新增在链表的尾部 p.next = newNode(hash, key, value, null);

        if (binCount >= TREEIFY_THRESHOLD - 1),当我们在链表尾部添加了元素后,这条语句会检查该链表是否已经达到了树化的阈值(8),如果达到了阈值,则会调用treeifyBin方法进行树化

情况2.3.2
         
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;

这里的条件判断表示,在链表迭代时,找到了与要插入的元素相同的元素相同(哈希值相同,key值一样或equals方法得true) ,那么将链表上该位置的元素替换为要插入的元素

扩容方法(resize)分析

原因

为了解决哈希冲突导致的链化影响查询效率问题,扩容可以缓解这个问题

源码分析

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

oldTab:表示扩容前的哈希表

oldCap:表示扩容前table数组的长度

oldThr:表示扩容前的扩容阈值,触发本次扩容的阈值

newCap:扩容之后table数组的大小

newThr:扩容之后下次再触发扩容的条件 

        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
        }

 if (oldCap > 0)条件如果成立说明,hashMap中的散列表已经初始化过了,是一次正常的扩容 

        if oldCap >= MAXIMUM_CAPACITY:如果扩容前的table数组大小已经达到了最大阈值,则不再扩容,且设置下次扩容条件为int最大值

        newCap通过oldCap左移一位获得,即oldCap乘以2,newCap小于数组最大值限制(1<<30)且扩容前的阈值>=16,这种情况下,则下次扩容的阈值等于当前阈值*2

        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;

这种情况一般用于传入指定长度构造方法时使用,初始化table,这里的oldThr即tableSizeFor方法计算出来的值

        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }

此时oldCap ==0,oldThr ==0

也就是调用空参的构造方法时会使用这里的newCap和newThr作为table的长度与扩容阈值

此时newCap为16,newThr为16*0.75 = 12

        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }

newThr为0时,通过newCap与loadFactor去计算出一个newThr

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

if (oldTab != null):说明hashMap本次扩容前,table不为null,也就是table已经指向了一个数组了

在for循环里,首先定义了Node<K,V> e 用来表示当前node节点

 if ((e = oldTab[j]) != null)

即说明当前的table数组位置中有元素,但具体是单个数据还是链表或者红黑树还并不知道

第一种情况:当前数组此下标下只有一个元素,没有发生过哈希冲突

if (e.next == null)

此时直接计算出当前元素应该存放在新数组的位置(哈希值&数组长度-1)然后放进去就可以了

第二种情况:当前数组此下标下已经树化

else if (e instanceof TreeNode)

第三种情况:该位置是链表结构

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

扩容后,会将链表分向数组其他位置,低位链表:存在扩容后的数组下标的位置,与当前数组下标位置一直,高位链表:存放在扩容之后的数组下标位置为 当前数组下标位置 + 扩容前数组的长度

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值