HashMap源码解析

本文详细介绍了HashMap的内部机制,包括默认初始容量、最大容量、加载因子、树化和反树化阈值。分析了put操作的流程,涉及扩容、树化条件以及键值对的存储策略。还探讨了HashMap如何处理冲突,以及key的不可变性以确保映射关系的稳定性。
变量和常量
1, static final int DEFAULT_INITIAL_CAPACITY = 1 << 4   //默认的初始容量
2, static final int MAXIMUM_CAPACITY = 1 << 30;   //最大容量
3,static final float DEFAULT_LOAD_FACTOR = 0.75f;   //默认加载因子  0.75
4,static final int TREEIFY_THRESHOLD = 8;   // 默认树化阈值8, 当树中的节点的个数达到这个阈值后,要考虑变为树
5,static final int MIN_TREEIFY_CAPACITY = 64; //最小树化容量  
    //当单个的链表的节点个数达到8,并且table的长度达到64,才会树化。
    //当单个的链表的节点个数达到8,但是table的长度未达到64,先扩容
6,static final int UNTREEIFY_THRESHOLD = 6;  //默认反树化阈值6, 当树种的节点的个数达到这个阈值后,要考虑变为链表   
7,transient Node<K,V>[] table;   // 数组
8,transient int size; 数组
9,transient int modCount;
//modCount 记录当前集合被修改的次数
(1)添加
(2)删除
这两个操作都会影响元素的个数。
当我们使用迭代器或者foreach遍历时,如果在foreach遍历时,自动调用迭代器的迭代方法
此时在遍历过程中调用了集合的add,remove方法时,modCount就会改变。
而迭代器记录的modCount是开始迭代之前的,如果两个不一致,就会报异常。
说明有两个线路(线程)同时操作集合。这种操作有风险,为了保证结果的正确性。
避免这样的情况发生,一旦发现modCount与exectedModCount不一致,立即报错。

此类的iterator和listIterator方法返回的迭代器是快速失败的:
在创建迭代器之后,除非是通过迭代器自身的remove或add方法从结构上对列表进行修改,        否则任何时间以任何方式对列表进行修改,迭代器都会抛出ConcurrentModificationException
因此,面对并发的修改,迭代器很快就会完全失败。
而不是冒着将来某个不确定时间发生任意不确定行为的风险。
    
    
    

    
            
    

10,int threshold;  阈值,当size达到阈值时,考虑扩容
11,final float loadFactor;  加载因子,影响扩容的频率

1、new HashMap()
*  public HashMap() {
*      //加载因子赋值为0.75
       this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted 其他字段都是默认值
       //threshold是0
       //table=null
       //size = 0
   }
2,put(key,value)
(1) 如果第一次添加时,把table初始化为长度为16的数组,threshold = 12
(2)如果不是第一次添加
①会考虑是否key重复,那么就替换value
②如果table[i] 下面不是树 ,统计table[i]的节点,添加之前达到7,考虑树化
    当单个链表的结点个数添加之前达到7,并且table的长度达到64,才会树化。
    当单个链表的结点个数添加之前达到7,table的长度未达到64,先扩容。
③table[i]下面已经是树,单独处理,直接把新的映射关系连接到树的叶子节点
④添加后,size达到threshold  还要扩容, 一旦扩容,就会调整所有的映射关系位置。  


HashMap

存储到HashMap中的映射关系(key,value),其中的key的hashCode值和equals方法非常重要。

1、hashCode值

hash算法是一种可以从任何数据中提取出其“指纹”的数据摘要算法,它将任意大小的数据映射到一个固定大小的序列上,这个序列被称为hash code、数据摘要或者指纹。比较出名的hash算法有MD5、SHA。hash是具有唯一性且不可逆的,唯一性是指相同的“对象”产生的hash code永远是一样的。

2、Entry数组

HashMap和Hashtable是散列表,其中维护了一个长度为2的幂次方的Entry类型的数组table,数组的每一个元素被称为一个桶(bucket),你添加的映射关系(key,value)被最终都封装为一个Map.Entry类型的对象,放到了某个table[index]桶中。使用数组的目的是查询和添加的效率高,可以根据索引直接定位到某个table[index]。

(1)数组元素类型:Map.Entry

JDK1.7

transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

static class Entry<K,V> implements Map.Entry<K,V> {

        final K key;

        V value;

        Entry<K,V> next;

        int hash;

  //...省略

}

JDK1.8

transient Node<K,V>[] table;

static class Node<K,V> implements Map.Entry<K,V> {

        final int hash;

        final K key;

        V value;

        Node<K,V> next;

   //...省略

}

(2)初始容量:16

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

(3)扩容为原来的2倍

JDK1.7

    void addEntry(int hash, K key, V value, int bucketIndex) {

        if ((size >= threshold) && (null != table[bucketIndex])) {

            resize(2 * table.length);

            hash = (null != key) ? hash(key) : 0;

            bucketIndex = indexFor(hash, table.length);

        }

        createEntry(hash, key, value, bucketIndex);

    }

JDK1.8

    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

        }

   //......此处省略其他代码

}

(4)那么HashMap是如何决定某个映射关系存在哪个桶的呢?

因为hash code是一个整数,而数组的长度也是一个整数,有两种思路:

①hash code值 % table.length会得到一个[0,table.length-1]范围的值,正好是下标范围,但是用%运算,不能保证均匀存放,可能会导致某些table[index]桶中的元素太多,而另一些太少,因此不合适。

②hash code值 & (table.length-1),因为table.length是2的幂次方,因此table.length-1是一个二进制低位全是1的数,所以&操作完,也会得到一个[0,table.length-1]范围的值。

JDK1.7

    static int indexFor(int h, int length) {

        // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";

        return h & (length-1);

    }

JDK1.8

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

        //....省略大量代码

}

3、HashMap中的散列函数hash()

JDK1.7和JDK1.8关于hash()的实现代码不一样,但是不管怎么样都是为了提高hash code值与 (table.length-1)的按位与完的结果,尽量的均匀分布。

JDK1.7

    final int hash(Object k) {

        int h = hashSeed;

        if (0 != h && k instanceof String) {

            return sun.misc.Hashing.stringHash32((String) k);

        }

        h ^= k.hashCode();

        h ^= (h >>> 20) ^ (h >>> 12);

        return h ^ (h >>> 7) ^ (h >>> 4);

    }

JDK1.8

    static final int hash(Object key) {

        int h;

        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

    }

这里用JDK1.8的示例分析一下:

那么,JDK1.8为什么要保留高16位呢?

因为一个HashMap的table数组一般不会特别大,至少在不断扩容之前,那么table.length-1的大部分高位都是0,直接用hashCode和table.length-1进行&运算的话,就会导致总是只有最低的几位是有效的,那么就算你的hashCode()实现的再好也难以避免发生碰撞,这时保留高16位的意义就体现出来了。它对hashcode的低位添加了随机性并且混合了高位的部分特征,显著减少了碰撞冲突的发生。

4、解决冲突

虽然从设计hashCode()到上面HashMap的hash()函数,都尽量减少冲突,但是仍然存在两个不同的对象返回的hashCode值相同,或者hashCode值就算不同,通过hash()函数计算后,得到的index也会存在大量的相同,因此key分布完全均匀的情况是不存在的。那么发生碰撞冲突时怎么办?

JDK1.8之间使用:数组+链表的结构。

JDK1.8之后使用:数组+链表/红黑树的结构。

5、JDK1.7的put存储过程

(1)几个常量和变量值的作用:

①默认负载因子

static final float DEFAULT_LOAD_FACTOR = 0.75f;

②负载因子

final float loadFactor;

③阈值

int threshold;

threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);

    void addEntry(int hash, K key, V value, int bucketIndex) {

        if ((size >= threshold) && (null != table[bucketIndex])) {                     

            resize(2 * table.length);  //扩容                                             

            hash = (null != key) ? hash(key) : 0;

            bucketIndex = indexFor(hash, table.length);

        }

        createEntry(hash, key, value, bucketIndex);

    }

(2)存储过程

A. 先判断table是否为空数组,如果是,先初始化数组,长度为16;

B. 判断key是null,index=0;如果key不是null,那么先计算hash(key),在通过处理过的hash值&(table.length-1)计算index,决定是在table[index],index在[0,table.length-1]范围内;

C. 判断table[index]桶下是否存在某个Entry的key与新的key的“相同”(hash值相同并且(满足key的地址相同或key的equals返回true)),如果是,用新的value替换原来的value;

D. 如果不存在,判断是否满足size达到阈值(threshold)并且table[index]不是null,如果是,先扩容;扩容会导致原来table中的所有元素都会重新计算位置,并调整存储位置;

E. 添加一个新的Entry对象至table[index](注意,这个index也是重新计算过的)中,并且把当前table[index]下的所有元素都连接到新的Entry的next下。

F. size++,元素个数增加

6、JDK1.8的put存储过程

(1)几个常量和变量值的作用:

①默认负载因子static final float DEFAULT_LOAD_FACTOR = 0.75f;

②负载因子final float loadFactor;

③阈值int threshold;

当size达到threshold阈值时,会扩容;

④树化阈值static final int TREEIFY_THRESHOLD = 8;

该阈值的作用是判断是否需要树化,树化的目的是为了提高查询效率;当某个链表的结点个数达到这个值时,可能会导致树化。

⑤树化最小容量值static final int MIN_TREEIFY_CAPACITY = 64;

当某个链表的结点个数达到8时,还要检查table的长度是否达到64,如果没有达到,先扩容解决冲突问题

⑥反树化阈值static final int UNTREEIFY_THRESHOLD = 6;

当删除了结点时,如果某棵红黑树的结点个数已经低于该值时,会把树重新变成链表,目的是减少复杂度。

(2)存储过程

A. 先计算key的hash值,如果key是null,hash值就是0,如果为null,使用(h = key.hashCode()) ^ (h >>> 16)得到hash值;

B. 如果table是空的,先初始化table数组;

C. 通过hash值计算存储的索引位置index = hash & (table.length-1)

D. 如果table[index]==null,那么直接创建一个Node结点存储到table[index]中即可

E. 如果table[index]!=null,并且table[index]是一个TreeNode结点,说明table[index]下是一棵红黑树,如果该树的某个结点的key与新的key“相同”(hash值相同并且(满足key的地址相同或key的equals返回true)),那么用新的value替换原来的value,否则将(key,value)封装为一个TreeNode结点,连接到红黑树中。

F. 如果table[index]不是一个TreeNode结点,说明table[index]下是一个链表,如果该链表中的某个结点的key与新的key“相同”,那么用新的value替换原来的value,否则需要判断table[index]下结点个数,如果没有达到TREEIFY_THRESHOLD(8)个,那么(key,value)将会封装为一个Node结点直接链接到链表尾部。

G. 如果table[index]下结点个数已经达到TREEIFY_THRESHOLD(8)个,那么再判断table.length是否达到MIN_TREEIFY_CAPACITY(64),如果没达到,那么先扩容,扩容会导致所有元素重新计算index,并调整位置;

H. 如果table[index]下结点个数已经达到TREEIFY_THRESHOLD(8)个并table.length也已经达到MIN_TREEIFY_CAPACITY(64),那么会将该链表转成一棵自平衡的红黑树,并将结点链接到红黑树中。

I. 如果新增结点而不是替换,那么size++,并且还要重新判断size是否达到threshold阈值,如果达到,还要扩容。

7、关于映射关系的key是否可以修改?

映射关系存储到HashMap中会存储key的hash值,这样就不用在每次查找时重新计算每一个Entry或Node(TreeNode)的hash值了,因此如果已经put到Map中的映射关系,再修改key的属性,而这个属性又参与hashcode值的计算,那么会导致匹配不上。

这个规则也同样适用于LinkedHashMap、HashSet、LinkedHashSet、Hashtable等所有散列存储结构的集合。

JDK1.7

transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

static class Entry<K,V> implements Map.Entry<K,V> {

        final K key;

        V value;

        Entry<K,V> next;

          int hash;                                                         

  //...省略

}

JDK1.8

transient Node<K,V>[] table;

static class Node<K,V> implements Map.Entry<K,V> {

       final int hash;                                                      

        final K key;

        V value;

        Node<K,V> next;

   //...省略

}

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值