HashMap源码解析之JDK1.8

首先来一张图
HashMap源码解析之JDK1.8_第1张图片

默认初始化容量1<<4  二进制中1左移四位是10000也就是2的四次方=16   

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

 

最大容量1<<30  二进制中1左移三十位就是230次方

static final int MAXIMUM_CAPACITY = 1 << 30;

 

负载因子 0.75 负载因子*容量就是扩容的阈(yu)static final float DEFAULT_LOAD_FACTOR = 0.75f;

 

链表转换为树的阈值

static final int TREEIFY_THRESHOLD = 8;

 

少于这个值的时候就用链表

static final int UNTREEIFY_THRESHOLD = 6;

 

有红黑树时容量小于这个值的时候就resize()(扩容两倍)

static final int MIN_TREEIFY_CAPACITY = 64;

 

Node就是一个链表,有四个参数,hash,key,value,以及next

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;

      }

 

 

默认我们创建HashMap()就是用的这个无参构造

public HashMap() {

        this.loadFactor = DEFAULT_LOAD_FACTOR; //初始化负载因子为0.75

}

 

有参构造(初始化容量和负载因子)

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

}

public HashMap(int initialCapacity) {

        this(initialCapacity, DEFAULT_LOAD_FACTOR);

}

 

>>>>>的区别 >>是右移,正数高位补0,负数高位补1

>>>是无符号右移,无论正负都在高位补0

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;

}

假设n=01000000|00000000(n也可以是大于214次方小于215次方之间的任意数)

   00100000|00000000          n>>>1

|  01000000|00000000         n

=  01100000|00000000

最后n就是01111111|11111111  return n+1 也就是10000000|00000000也就是215次方

所以这里阈值其实是大于等于(当n=1时会等于)初始容量最近的2N次方的值,这里阈值会大于容量值,所以在第一次调用put的时候map容量会扩容成为阈值,然后扩容后的阈值会重新算

//创建一个内容为参数 m 的内容的哈希表

public HashMap(Map<? extends K, ? extends V> m) {

        this.loadFactor = DEFAULT_LOAD_FACTOR;

        putMapEntries(m, false);

}

 

final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {

        int s = m.size();

        if (s > 0) {

    //数组还是空,初始化参数

            if (table == null) { // pre-size

                float ft = ((float)s / loadFactor) + 1.0F;

                int t = ((ft < (float)MAXIMUM_CAPACITY) ?

                         (int)ft : MAXIMUM_CAPACITY);

                if (t > threshold)

                    threshold = tableSizeFor(t);

            }

    //数组不为空,超过阈值就扩容

            else if (s > threshold)

                resize();

            for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {

                K key = e.getKey();

                V value = e.getValue();

//先经过 hash() 计算位置,然后复制指定 map 的内容

                putVal(hash(key), key, value, false, evict);

            }

        }

    }

 

 

最后我们来看put方法

public V put(K key, V value) {

        return putVal(hash(key), key, value, false, true);

}

调用了putVal方法,其中调用了一个hash(key)方法作为hash

为什么不用key.hashcode()要额外去写一个hash(key)的方法呢?

static final int hash(Object key) {

        int h;

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

}

首先分析key.hashCode()得出一个32位二进制数假设为00000000|00000100|10001000|10000000

^符号是异或的意思,任何数异或0等于他本身,本身和本身异或等于0

   
   00000000|00000100|10001000|10001111

^  00000000|00000000|00000000|00000100(无符号右移16)

 = 00000000|00000100|10001000|10001011

这和数据下标取值有关系,先看putVal方法中tab[i = (n - 1) & hash]

这行代码就是我们的key,value存放地位置,也就是数组的索引值

n我们取默认16&我们的hash值

假设hash不做处理

    (00000000|00000100|10001000|10001011做了处理之后会有些差异)

   00000000|00000100|10001000|10001111

&  00000000|00000000|00000000|00001111

=  00000000|00000000|00000000|00001111

也就是高16位可能被屏蔽,即使屏蔽了也可移算出索引值,但是会减少差异性,这就是将性能做到极致的一种表现

然后,为什么我们默认是16呢?

假设是17

那么n-1=16

  00000000|00000100|10001000|10001011

& 00000000|00000000|00000000|00010000

= 00000000|00000000|00000000|00000000

无论我们后4位是什么都是0,所以索引只会有两个值,一个是0,一个是16,取16的时候,n-11111,那么就会出现0-15种可能,这也是前面定义tableSizeFor要取大于等于容量的最接近2次方数的原因

知道索引怎么算了,再来看看put方法调用的putVal(hash(key), key, value, false, true)方法

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,

                   boolean evict) {

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

//如果我们调用的是无参构造,table是没有被初始化的,那么默认就是null

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

//执行resize()方法,这里是初始化table,容量设位16,阈值设为16*0.75f

            n = (tab = resize()).length;

     //如果Node数组索引上没有值,就直接赋值

        if ((p = tab[i = (n - 1) & hash]) == null)

            tab[i] = newNode(hash, key, value, null);

 

      //Node数组上有值了(有值那么就是链表或者红黑树) 

else {

            Node<K,V> e; K k;

         //这里p已经在上个if语句里面赋值了,就是tab[i]这个Node对象

        //如果hash相同key相同e=tab[i]

            if (p.hash == hash &&

                ((k = p.key) == key || (key != null && key.equals(k))))

                e = p;

        //tab[i]属于红黑树,就放入树中

            else if (p instanceof TreeNode)

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

       //其他情况下,就是有值的情况下,key又不相同,那么可能是只有一个元素Node,或者是我们说的链表

            else {

        //一个死循环,每次循环完+1

                for (int binCount = 0; ; ++binCount) {

        //这里e在if里面赋值e=tab[i].next

        //如果Node元素next没有值,直接放在下面,链表元素有八个了,那么我们就转换为红黑树,没有就不转换,然后跳出循环

                    if ((e = p.next) == null) {

                        p.next = newNode(hash, key, value, null);

            //如果binCont>=7,也就是0-7循环了八次,也就是链表元素有八个了,那么我们就转换为红黑树

if (binCount >= TREEIFY_THRESHOLD - 1)                              treeifyBin(tab, hash);

                        break;

                    }

            //如果key值相同,直接跳出循环

                    if (e.hash == hash &&

                        ((k = e.key) == key || (key != null && key.equals(k))))

                        break;

            //如果node又不是链表最后一个,key也不相同,就把e赋值给p,也就是拿下一个Node去循环,然后取下一个取重复判断

                    p = e;

                }

            }

        //只有当链表上某个Node key值一样时,e才是不等于null,key一样就是替换value值,然后返回旧值

            if (e != null) { // existing mapping for key

                V oldValue = e.value;

                if (!onlyIfAbsent || oldValue == null)

                    e.value = value;

                afterNodeAccess(e);

                return oldValue;

            }

        }

    //modCount用于记录HashMap的修改次数

        ++modCount;

    // size是指当前hashmap中数据的个数,大于阈值的时候,resize()方法扩容

        if (++size > threshold)

            resize();

    //预留给LinkedHashMap的,这里没有作用

        afterNodeInsertion(evict);

    //key不相等的时候,旧值就是null

        return null;

}

最后我们看看resize()这个方法,前面第一次进来的时候调用了,大于阈值的时候也调用了。

final Node<K,V>[] resize() {

    //table第一次进来的时候是没有初始化的,所以就是null,后面进来的时候就是我们还没放这个Node的时候的原Node数组

        Node<K,V>[] oldTab = table;

    //旧容量

        int oldCap = (oldTab == null) ? 0 : oldTab.length;

    //旧阈值

        int oldThr = threshold;

        int newCap, newThr = 0;

    //旧容量大于0时,也就是初始化过了时

        if (oldCap > 0) {

        //如果容量超过了最大容量就取阈值为int的最大值,然后返回不做任务操作

            if (oldCap >= MAXIMUM_CAPACITY) {

                threshold = Integer.MAX_VALUE;

                return oldTab;

            }

        //新容量=老容量*2

        //新容量小于最大容量并且老容量大于等于默认容量16,新阈值=老阈值*2

            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&

                     oldCap >= DEFAULT_INITIAL_CAPACITY)

                newThr = oldThr << 1; // double threshold

        }

    //旧阈值大于0,新容量就等于老阈值,这里是调用无参构造之外的构造函数时,会根据tableSizeFor给阈值赋值,阈值其实是大于等于初始容量最近的2的N次方

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

            newCap = oldThr;

    //其他情况,就是没有初始化过,也就是我们传统的调用无参的时候,容量变位16,阈值变为16*0.75f=12

        else {             

            newCap = DEFAULT_INITIAL_CAPACITY;

            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);

        }

    //如果新阈值还没赋值,就判断新容量是否大于最大容量,大于就取int最大值,小于就取新容量*负载因子0.75

        if (newThr == 0) {

            float ft = (float)newCap * loadFactor;

            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?

                      (int)ft : Integer.MAX_VALUE);

        }

    //扩容后的阈值赋值给全局变量阈值

        threshold = newThr;

        @SuppressWarnings({"rawtypes","unchecked"})

    //这里就是hashmap的本质,new一个新容量的map并且赋值给全局数组table

            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];

        table = newTab;

    //原来的map不为空时,也就是我们put过值了时

        if (oldTab != null) {

        //遍历这个map

            for (int j = 0; j < oldCap; ++j) {

                Node<K,V> e;

        //拿到每个索引上的Node,e= oldTab[j]

        //如果这个索引上有值

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

            //这一部是把原来node数组这个位置变空,为了垃圾更快回收

                    oldTab[j] = null;

            //如果Node的next是空,也就是不是链表,也不是红黑树,直接把这个元素赋值到新的Node数组中

                    if (e.next == null)

                        newTab[e.hash & (newCap - 1)] = e;

            //如果这个Node是红黑树,那么就拆分然后重新放入新的node数组中

                    else if (e instanceof TreeNode)

                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

            //如果是链表

                    else { // preserve order

                //原来的Node数组防值的时候会有个索引,扩容后的数组也会有个索引,下面就会判断这个索引是否发生改变,lohead这个是索引没发生改变时候的头元素,hiHead就是发生改变后的头元素

                        Node<K,V> loHead = null, loTail = null;

                        Node<K,V> hiHead = null, hiTail = null;

                        Node<K,V> next;

             //这里有个判断,一直循环到Node元素next值为空才跳出,就是链表最后一个值的时候就跳出,注意e是先在while里面重复赋值的

                        do {

                            next = e.next;

//这里用了一种简单的算法判断索引是否发生改变,e.hash & oldCap==0时就是没有发生改变,如图,hash1,hash2在n容量时的索引和扩容之后的索引的变化,我们只需要看hash的高位(标红)的是否为0,如果是0,n&hash==0,索引不会变化

 

 

假设原容量是16

n-1                 00001111            2n-1          00011111

        &           00010101(hash1)               &   00010101

索引值               00000101                          00010101  变成101+n

n-1                 00001111            2n-1          00011111

        &           00000101(hash2)                   00000101(hash2)

                    00000101 索引值                    00000101 没有变化

 

                            if ((e.hash & oldCap) == 0) {

                //循环第一次进来,lohead头确定好

                                if (loTail == null)

                                    loHead = e;

                //不是第一次进来

                                else

                                    loTail.next = e;

//不管是不是第几次,lotail都定义为这个node元素

//这里有点绕,假设有个链表aa下面挂一个bb,

//第一次e=aa,lohead=aa,lotail=aa,lotail.next=aa

//第二次因为next=e.next那么next=bb,while循环里面e=next,所以e=bb,lohead=aa,//lotail=bb,lotail.next=bb

                                loTail = e;

                            }

                //同理,索引发生变化时一样

                            else {

                                if (hiTail == null)

                                    hiHead = e;

                                else

                                    hiTail.next = e;

                                hiTail = e;

                            }

                        } while ((e = next) != null);

//如果lotail不为空,索引不变,数组把头放进取即可,因为余下链表只跟着头走

                        if (loTail != null) {

                            loTail.next = null;

                            newTab[j] = loHead;

                        }

//如果lotail不为空,索引变化了,数组索引加旧容量,再把头放进取即可                        if (hiTail != null) {

                            hiTail.next = null;

                            newTab[j + oldCap] = hiHead;

                        }

                    }

                }

            }

        }

        return newTab;

    }

在这里插入图片描述

你可能感兴趣的:(jdk源码解析,java,hashmap,链表)