HashMap底层源码解析

 HashMap底层源码解析_第1张图片

HashMap继承了AbstractMap这个抽象类 并且实现了Map这个接口,可以实现clone和序列化


底层数据结构 : 数组 + 单链表 + 红黑树

HashMap底层源码解析_第2张图片

 【说明】 每一个数组+ 单链表/红黑树 叫做桶 也叫做段


 HashMap底层源码解析_第3张图片


定义了hash表所对应的数组的长度

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16   

HashMap底层源码解析_第4张图片

hash表中数组的最大长度

static final int MAXIMUM_CAPACITY = 1 << 30;  

扩充因子,数组里存放的值达到数组长度的75%,就开始扩容

static final float DEFAULT_LOAD_FACTOR = 0.75f; 

单链表的长度,如果超过8,就把单链表转为红黑树

final int TREEIFY_THRESHOLD = 8; 

桶中红黑树立元素个数,小等于6时,转为单链表

static final int UNTREEIFY_THRESHOLD = 6; 


1、hash表中数组的作用

        存放点链表或树结构的首地址(不存具体数据)


2、链表转红黑树的条件为什么是8?

      HashMap在JDK1.8及以后的版本中引⼊了红⿊树结构,若桶中链表元素个数⼤于等于8时,链表转换成树结构;若桶中链表元素个数⼩于等于6时,树结构还原成链表。因为红⿊树的平均查找⻓度是log(n),⻓度为8的时候,平均查找⻓度为3,如果继续使链表,平均查找⻓度为8/2=4,这才有转换为树的必要。链表⻓度如果是⼩于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和⽣成树的时间并不会太短。还有选择6和8,中间有个差值7可以有效防⽌链表和树频繁转换。假设⼀下,如果设计成链表个数超过8则链表转换成树结构,链表个数⼩于8则树结构转换成链表,如果⼀个HashMap不停的插⼊、删除元素,链表个数在8左右徘徊,就会频繁的发⽣树转链表、链表转树,效率会很低。

也就是说如果一开始为6此时是链表 那么再插入一个元素变为7的时候依旧是链表 再增加一个 变为8的时候才转为红黑树 

但是如果 一开始为8此时为红黑树 那么删除一个元素变7的时候依旧是红黑树 再删除一个 变为6的时候才转换为链表


3、HashMap如何存储key-value的形式

因为在HashMap源码中 有一个静态内部类Node ,当每存入一个值的时候就会创建也就是new出一个Node节点(对象),该对象中有K属性和V属性,分别来存储Key和Value

HashMap底层源码解析_第5张图片


4、HashMap存放数据的特点

        无序、key值唯一,如果Key重复,value值会进行覆盖

         在存值的时候,首先对Key进行hash值计算,计算的结果就是hash表中数组的存放的位置


5、Hash值的算法        

HashMap底层源码解析_第6张图片

 hashMap的key允许存null值,具体来说 hashMap的key和value都可以存null值

先定义变量h用来接收hashCode值

key为null时

当key为null的时候就将元素存储在数组中0这个位置

HashMap底层源码解析_第7张图片

key不为null时

首先计算出key的hashcode值 

将hashcode的值 无符号 向右移16位做异或运算,此时得到hash值

再将hash值与数组长度-1去做与运算 

此时得出来的这个结果是一个0~数组长度中间的一个数,这个数就是数组中的索引,也就是地址在数组中存放的位置

【异或运算】相同为0不同为1

【与运算】 0&0=0;0&1=0;1&0=0;1&1=1


6、Hash值存放元素的方式

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node[] tab; Node 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 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)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;
    }

1、key调用hash算法计算出存储值在数组中的存放位置 

2、判断当前的存储位置是否有元素

      如果当前数组的存放位置为null 证明没有存放元素 那么就new Node(key,value) 将new Node之后的地址存放在数组的元素里 将key       和 value的值存放在链表中

      如果当前位置有元素(hash值发生冲突 不同key经过hash计算结果可能是一样的),就顺着单链表,从首元素开始,逐个使用equals比       较key值是否相等

             如果key的equals值都不相等,那么new Node节点,用来存放key和value的值,把这个节点所对应的地址放在单链表的末尾,               存放后,判断单链表的长度是否大于 8,如果是,把单链表转为红黑树,不是的话依旧是链表

             如果通过equals比较完,当前key和某个链表中Node的key相等(hash冲突),则使用当前的value去覆盖掉原有的value

3、存放完毕后,判断阈值:阈值=数组长度 * 0.75f 如果hashMap中元素的个数已经超过阈值,数组则进行扩容,每次       数组的长度扩容到原来的2倍

             


7、为什么数组长度要扩容2倍

扩容2倍后,计算出的hash值锁产生的hash冲突的几率最小。

扩容后,由于数组长度发生了改变,所有元素都要重新计算hash值(与数组长度-1做与运算),存放位置可能会发生改变

如果存放元素超过12个,最后new HashMap的时候指定数组的长度  (用存放的个数除以12)

因为扩容所有元素都要重新计算hash值所以我们应该尽量减少数组的扩容,根据存放的元素个数在最开始的时候就定义好数组的长度,具体执行的方法如下

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


8、HashMap和HashTable的区别

1. Hashtable是线程安全,而HashMap则非线程安全,Hashtable的实现方法里面大部分都添加了synchronized关键字来确保线程同步,因此相对而言HashMap性能会高一些,在多线程环境下若使用HashMap需要使用Collections.synchronizedMap()方法来获取一个线程安全的集合。

2. HashMap的键和值都可以为null,而Hashtable的键值都不能为null。

3. HashMap的初始容量为16,Hashtable初始容量为11,两者的填充因子默认都是0.75。HashMap扩展容量是当前容量翻倍即:capacity*2,Hashtable扩展容量是容量翻倍+1即:capacity*2+1(关于扩容和填充因子后面会讲)

4. 两者的哈希算法不同,HashMap是先对key(键)求hashCode码,然后再把这个码值得高位和低位做异或运算,源码如下:

HashMap: 得到key值得hashcode, 

​      对hashcode值异或运算

​      对异或计算的结果  和  初始容量(数组大小)-1做&运算

static final int hash(Object key) {

​    int h;

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

  }

i = (n - 1) & hash

然后把hash(key)返回的哈希值与HashMap的初始容量(也叫初始数组的长度)减一做&(与运算)就可以计算出此键值对应该保存到数组的那个位置上(hash&(n-1))。这里为什么不用key本身的hashcode方法,而又是右移动16位又是异或操作。开发人员这样做的目的是什么呢?是当数组容量很小的时候,计算元素在数组中的位置(n-1)&hash,只用到了hash值的低位,这样当不同的hash值低位相同,高位不同的时候会产生冲突。实际上的hash值将hashcode低16位与高16位做异或运算,相当于混合了高位和低位,增加了随机性。当然是冲突越少越好,元素的分布越随机越好。

而Hashtable计算位置的方式如下:

int hash = key.hashCode();int index = (hash & 0x7FFFFFFF) % tab.length;

直接计算key的哈希码,然后与2的31次方做&(与运算),然后对数组长度取余数计算位置。

你可能感兴趣的:(散列表,数据结构)