HashMap知识点(一)

HashMap知识点(一)

一、HashMap扩容机制原理

1、JDK1.7版本扩容机制

生成一个新的数组,将原数组的元素全都转移到新的数组上

resize方法源码

/**
* 分析:resize(2 * table.length)
* 作用:当容量不足时(容量 > 阈值),则扩容(扩到2倍)
*/   
void resize(int newCapacity) {  
    // 1. 保存旧数组(old table)
    Entry[] oldTable = table; 
    // 2. 保存旧容量(old capacity ),即数组长度
    int oldCapacity = oldTable.length;
    // 3. 若旧容量已经是系统默认最大容量了,那么将阈值设置成整型的最大值,退出    
    if (oldCapacity == MAXIMUM_CAPACITY) {
        // 修改扩容阀值  
        threshold = Integer.MAX_VALUE;
        return;  
    }  
    // 4. 根据新容量(2倍容量)新建1个数组,即新table  
    Entry[] newTable = new Entry[newCapacity];  
    // 5. 将旧数组上的数据(键值对)转移到新table中,从而完成扩容。initHashSeedAsNeeded(newCapacity)这个方法用来根据新的数组长度来重新初始化Hash种子
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    // 6. 新数组table引用到HashMap的table属性上
    table = newTable;  
    // 7. 重新设置阈值,如果阈值超过了HashMap最大容量大小,则直接将阈值设置为 MAXIMUM_CAPACITY + 1
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
} 

initHashSeedAsNeeded()方法源码

这个方法在JDK1.7中inflateTable()初始化哈希表方法和resize()扩容方法中都有出现。这个方法作用是初始化Hash种子。在JDK1.7中计算hash值的方法需要使用Hash种子来参与运算,进而提高计算出来的hash值的散列性,最大限度减少哈希冲突。下面就来简单讲一下这个方法:

 * 这个方法用来根据新的数组长度来重新初始化Hash种子,好的Hash种子能提高计算Hash时结果的散列性,最大限度减少哈希冲突。
 * @param capacity 根据传入的容量大小来进行重新初始化Hash种子
 * @return 返回true说明已经根据传入的容量大小重新初始化了Hash种子,此时以前根据旧的Hash种子计算出来的Hash值就需要进行rehash了。
 *         返回false说明并没有根据传入的容量大小进行重新初始化Hash种子
 */
final boolean initHashSeedAsNeeded(int capacity) {
    // 首先会判断hashSeed是否不等于0,因为hashSeed一开始是0,所以此处是false
    boolean currentAltHashing = hashSeed != 0;
    // 这行代码是判断vm是否启动 且 容量到达一个值ALTERNATIVE_HASHING_THRESHOLD,这个值是可以自己去设定,不设定的话是默认的Integer.MaxValue 。 假设我们初始化容量capacity = 16,设置ALTERNATIVE_HASHING_THRESHOLD值为 3,那么这行代码会为true
    boolean useAltHashing = sun.misc.VM.isBooted() &&
        (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
    //亦或 ^ 的意思是 不相同则返回true,此时switching=true,那么hashSeed就会重新去计算hash种子,以便计算hash时增加散列性,
    boolean switching = currentAltHashing ^ useAltHashing;
    if (switching) {
        // 重新设置了Hash种子
        hashSeed = useAltHashing
            ? sun.misc.Hashing.randomHashSeed(this)
            : 0;
    }
    return switching;
}
 
final int hash(Object k) {
    // 设置了哈希种子
    int h = hashSeed;
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }
    // Hash种子参与到了key的Hash值计算当中
    h ^= k.hashCode();
    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

addEntry方法

void addEntry(int hash,k key,v vaule,int bucketIndex){
    //threshole为扩容阈值,及最大值*加载因子
    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);
}

transfer方法

void transfer(Entry[] newTable,boolean rehash){
    int newCapacity = newTable.length;
    for(Entry<K,V> e : table){
        while(nul != e){
            Entry<K,V> next = e.next;
            if(rehash){
                e.hash = null == e.key ? 0:hash(e.key);
            }
            int i= indexFor(e.hash, newCapacity);
            e.next = newTable[i];
            newTable[i] = e;
            e= next;
        }
    }
}

扩容后原来的元素只会在新数组的原位置或原位置+原数组大小处

new =old|new = old + 16

2、JDK1.8版本扩容机制

方法执行流程

**resize()****方法执行流程**

java8 resize方法源码

/**
 * Initializes or doubles table size.  If null, allocates in
 * accord with initial capacity target held in field threshold.
 * Otherwise, because we are using power-of-two expansion, the
 * elements from each bin must either stay at same index, or move
 * with a power of two offset in the new table.
 * 初始化或把table容量翻倍。如果table是空,则根据threshold属性的值去初始化HashMap的容                
 * 量。如果不为空,则进行扩容,因为我们使用2的次幂来给HashMap进行扩容,所以每个桶里的元素 
 * 必须保持在原来的位置或在新的table中以2的次幂作为偏移量进行移动
 * @return 返回Node<K, V>数组
 */
final Node<K,V>[] resize() {
    // 创建一个临时变量,用来存储当前的table
    Node<K,V>[] oldTab = table;
    // 获取原来的table的长度(大小),判断当前的table是否为空,如果为空,则把0赋值给新定义的oldCap,否则以table的长度作为oldCap的大小
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    // 创建临时变量用来存储旧的阈值,把旧table的阈值赋值给oldThr变量
    int oldThr = threshold;
    // 定义变量newCap和newThr来存放新的table的容量和阈值,默认都是0
    int newCap, newThr = 0;    
    // 判断旧容量是否大于0            
    if (oldCap > 0) {                            
        // 判断旧容量是否大于等于 允许的最大值,2^30
        if (oldCap >= MAXIMUM_CAPACITY) {    
            // 以int的最大值作为原来HashMap的阈值,这样永远达不到阈值就不会扩容了
            threshold = Integer.MAX_VALUE; 
            // 因为旧容量已经达到了最大的HashMap容量,不可以再扩容了,将阈值变成最大值之后,将原table返回       
            return oldTab;
        }
        // 如果原table容量不超过HashMap的最大容量,将原容量*2 赋值给变量newCap,如果newCap不大于HashMap的最大容量,并且原容量大于HashMap的默认容量
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                    oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 将newThr的值设置为原HashMap的阈值*2
            newThr = oldThr << 1; // double threshold
    }
    // 如果原容量不大于0,即原table为null,则判断旧阈值是否大于0 
    else if (oldThr > 0) // 如果原table为Null且原阈值大于0,说明当前是使用了构造方法指定了容量大小,只是声明了HashMap但是还没有真正的初始化HashMap(创建table数组),只有在向里面插入数据才会触发扩容操作进而进行初始化
        // 将原阈值作为容量赋值给newCap当做newCap的值。由之前的源码分析可知,此时原阈值存储的大小就是调用构造函数时指定的容量大小,所以直接将原阈值赋值给新容量
        newCap = oldThr;
    // 如果原容量不大于0,并且原阈值也不大于0。这种情况说明调用的是无参构造方法,还没有真正初始化HashMap,只有put()数据的时候才会触发扩容操作进而进行初始化
    else {               // zero initial threshold signifies using defaults
        // 则以默认容量作为newCap的值
        newCap = DEFAULT_INITIAL_CAPACITY;
        // 以初始容量*默认负载因子的结果作为newThr值
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 经过上面的处理过程,如果newThr值为0,说明上面是进入到了原容量不大于0,旧阈值大于0的判断分支。需要单独给newThr进行赋值
    if (newThr == 0) {
        // 临时阈值 = 新容量 * 负载因子
        float ft = (float)newCap * loadFactor;
        // 设置新的阈值 保证新容量小于最大总量   阈值要小于最大容量,否则阈值就设置为int最大值
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                    (int)ft : Integer.MAX_VALUE);
    }
    // 将新的阈值newThr赋值给threshold,为新初始化的HashMap来使用
    threshold = newThr;
    // 初始化一个新的容量大小为newCap的Node数组
    @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    // 将新创建的数组赋值给table,完成扩容后的新数组创建
    table = newTab;
    // 如果旧table不为null,说明旧HashMap中有值
    if (oldTab != null) {
        // 如果原来的HashMap中有值,则遍历oldTab,取出每一个键值对,存入到新table        
        for (int j = 0; j < oldCap; ++j) {
            // 创建一个临时变量e用来指向oldTab中的第j个键值对,
            Node<K,V> e;
            // 将oldTab[j]赋值给e并且判断原来table数组中第j个位置是否不为空
            if ((e = oldTab[j]) != null) {    
                // 如果不为空,则将oldTab[j]置为null,释放内存,方便gc
                oldTab[j] = null;   
                // 如果e.next = null,说明该位置的数组桶上没有连着额外的数组          
                if (e.next == null)
                    // 此时以e.hash&(newCap-1)的结果作为e在newTab中的位置,将e直接放置在新数组的新位置即可          
                    newTab[e.hash & (newCap - 1)] = e; 
                // 否则说明e的后面连接着链表或者红黑树,判断e的类型是TreeNode还是Node,即链表和红黑树判断
                else if (e instanceof TreeNode)  
                    // 如果是红黑树,则进行红黑树的处理。将Node类型的e强制转为TreeNode,之所以能转换是因为TreeNode 是Node的子类
                    // 拆分树,具体源码解析会在后面的TreeNode章节中讲解
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); 
                // 当前节不是红黑树,不是null,并且还有下一个元素。那么此时为链表   
                else { // preserve order 
                    /*
                        这里定义了五个Node变量,其中lo和hi是,lower和higher的缩写,也就是高位和低位,
                        因为我们知道HashMap扩容时,容量会扩到原容量的2倍,
                        也就是放在链表中的Node的位置可能保持不变或位置变成 原位置+oldCap,在原位置基础上又加了一个数,位置变高了,
                        这里的高低位就是这个意思,低位指向的是保持原位置不变的节点,高位指向的是需要更新位置的节点
                    */
                    // Head指向的是链表的头节点,Tail指向的是链表的尾节点
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    // 指向当前遍历到的节点的下一个节点
                    Node<K,V> next;
                    // 循环遍历链表中的Node
                    do {                   
                        next = e.next;
                        /*
                            如果e.hash & oldCap == 0,注意这里是oldCap,而不是oldCap-1。
                            我们知道oldCap是2的次幂,也就是1、2、4、8、16...转化为二进制之后,
                            都是最高位为1,其它位为0。所以oldCap & e.hash 也是只有e.hash值在oldCap二进制不为0的位对应的位也不为0时,
                            才会得到一个不为0的结果。举个例子,我们知道10010 和00010 与1111的&运算结果都是 0010  ,
                            但是110010和010010与10000的运算结果是不一样的,所以HashMap就是利用这一点,
                            来判断当前在链表中的数据,在扩容时位置是保持不变还是位置移动oldCap。
                        */
                        // 如果结果为0,即位置保持不变  
                        if ((e.hash & oldCap) == 0) {  
                            // 如果是第一次遍历    
                            if (loTail == null)      
                                // 让loHead = e,设置头节点     
                                loHead = e;               
                            else
                                // 否则,让loTail的next = e
                                loTail.next = e;   
                            // 最后让loTail = e       
                            loTail = e;                   
                        }
                        /*
                            其实if 和else 中做的事情是一样的,本质上就是将不需要更新位置的节点加入到loHead为头节点的低位链表中,将需要更新位置的节点加入到hiHead为头结点的高位链表中。
                            我们看到有loHead和loTail两个Node,loHead为头节点,然后loTail是尾节点,在遍历的时候用来维护loHead,即每次循环,
                            更新loHead的next。我们来举个例子,比如原来的链表是A->B->C->D->E。
                            我们这里把->假设成next关系,这五个Node中,只有C的hash & oldCap != 0 ,
                            然后这个代码执行过程就是:
                            第一次循环: 先拿到A,把A赋给loHead,然后loTail也是A
                            第二次循环: 此时e的为B,而且loTail != null,也就是进入上面的else分支,把loTail.next =                     
                                        B,此时loTail中即A->B,同样反应在loHead中也是A->B,然后把loTail = B
                            第三次循环: 此时e = C,由于C不满足 (e.hash & oldCap) == 0,进入到了我们下面的else分支,其 
                                        实做的事情和当前分支的意思一样,只不过维护的是hiHead和hiTail。
                            第四次循环: 此时e的为D,loTail != null,进入上面的else分支,把loTail.next =                     
                                        D,此时loTail中即B->D,同样反应在loHead中也是A->B->D,然后把loTail = D
                        */
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    // 遍历结束,即把table[j]中所有的Node处理完
                    // 如果loTail不为空,也保证了loHead不为空
                    if (loTail != null) {    
                        // 此时把loTail的next置空,将低位链表构造完成    
                        loTail.next = null;    
                        // 把loHead放在newTab数组的第j个位置上,也就是这些节点保持在数组中的原位置不变
                        newTab[j] = loHead;      
                    }
                    // 同理,只不过hiHead中节点放的位置是j+oldCap
                    if (hiTail != null) {       
                        hiTail.next = null;
                        // hiHead链表中的节点都是需要更新位置的节点
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    // 最后返回newTab
    return newTab;            
}

3、JDK1.7和JDK1.8的区别

img

二、HashMap和HashTable的区别?底层实现是什么?

1、HashMap和HashTable的区别

(1)最基本的区别HashTable是线程安全的,HashMap是线程不安全的,因为HashTable的每一个方法都使用synchronized关键字修饰。HashTable效率低下,现已不常使用,多使用CurrentHashMap.

(2)HashMap允许key和value为null,而HashTable不允许

2、底层实现是什么

jdk8开始链表高度到8、数组长度超过64,链表转换为红黑树,元素以内部类Node结点存在

  • 计算key的hash值,二次hash然后对数组长度取模,对应到数组下标

  • 如果没有产生hash冲突(下标位置没有元素),则直接创建Node数组

  • 如果产生hash冲突,先进行equal比较,相同则取代元素,不同,则判断链表高度插入链表,链表高度达到8,并且数组长度到64则转变为红黑树,长度低于6则将红黑树转回链表

  • key为null,存在下标0的位置

三 、HashMap里put方法的实现流程

1、put方法的作用和执行流程

HashMap 只提供了 put 用于添加元素,putval也是使用的默认修饰符,因此只能被本类或者该包下的类访问到,所以putVal 方法只是给 put 方法调用的一个方法,并没有提供给用户使用。

对 putVal 方法添加元素的分析如下:

1、如果定位到的数组位置没有元素,就直接插入。
2、如果定位到的数组位置有元素就和要插入的 key 比较,如果 key 相同就直接覆盖,如果 key 不相同,就判断 p 是否是一个树节点,如果是就调用e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value)将元素添加进入。如果不是就遍历链表插入(插入的是链表尾部)。

img

2、put()和putVal()源码

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
 
 
 /**
 * Implements Map.put and related methods.
 * 实现了map的put和相关方法
 * @param hash  key的hash值(key的hash高16位+高16位与低16位的异或运算)
 * @param key 键
 * @param value 值  
 * @param onlyIfAbsent onlyIfAbsent为true的时候不要修改已经存在的值,如果onlyIfAbsent为false,当插入的元素已经在HashMap中已经拥有了与其key值和hash值相同的元素,仍然需要把新插入的value值覆盖到旧value上。如果nlyIfAbsent为true,则不需要修改
 * @param evict evict如果为false表示构造函数调用
 * @return 返回旧的value值(在数组桶或链表或红黑树中找到存在与插入元素key值和hash值相等的元素,就返回这个旧元素的value值),如果没有发现相同key和hash的元素则返回null
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    // tab用来临时存放数组table引用   p用来临时存放数组table桶中的bin
    // n存放HashMap容量大小   i存放当前put进HashMap的元素在数组中的位置下标
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // table未初始化或者长度为0,进行扩容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    // 桶中已经存在元素
    else {
        // e记录当前节点  k记录key值
        Node<K,V> e; K k;
        // 比较桶中第一个元素(数组中的结点)的hash值相等,key相等
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
                // 将第一个元素赋值给e,用e来记录。直接将插入的新元素覆盖旧元素
                e = p;
        // hash值不相等,即key不相等并且该节点为红黑树结点,将元素插入红黑树
        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);
                    // 结点数量达到阈值(默认为 8 ),执行 treeifyBin 方法
                    // 这个treeifyBin()方法会根据 HashMap 数组情况来决定是否转换为红黑树。
                    // 只有当数组长度大于或者等于 64 的情况下,才会执行转换红黑树操作,以减少执行效率。否则,就是只是对数组扩容。
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
					// 树化操作
                        treeifyBin(tab, hash);
                    // 跳出循环  此时e=null,表示没有在链表中找到与插入元素key和hash值相同的节点
                    break;
                }
                // 判断链表中结点的key值和Hash值与插入的元素的key值和Hash值是否相等
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    // 若相等,则不用将其插入了,直接跳出循环
                    break;
                // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
                p = e;
            }
        }
        // 当e!=null时,表示在数组桶或链表或红黑树中存在key值、hash值与插入元素相等的结点。此时就直接用原有的节点就可以了,不用插入新的元素了。此时e就代表原本就存在于HashMap中的元素
        if (e != null) {
            // 记录e的value,也就是旧value值
            V oldValue = e.value;
            // onlyIfAbsent为false或者旧值为null,则需要用新的value值对旧value值进行覆盖
            if (!onlyIfAbsent || oldValue == null)
                //用新值替换旧值
                e.value = value;
            // 替换旧值时会调用的方法(默认实现为空)
            afterNodeAccess(e);
            // 返回旧值
            return oldValue;
        }
    }
    // 结构性修改,记录HashMap被修改的次数,主要用于多线程并发时候
    ++modCount;
    // 实际大小大于阈值则扩容    ++size只有在插入新元素才会执行,如果发现HashMap中已经存在了相同key和hash的元素,就不会插入新的元素,在上面就已经执行return了,也就不会改变size大小
    if (++size > threshold)
        resize();
    // 插入成功时会调用的方法(默认实现为空)
    afterNodeInsertion(evict);
    // 没有找到原有相同key和hash的元素,则直接返回Null
    return null;
}

3、对比JDK1.7的put()方法源码

对于JDK1.7的 put 方法的分析如下:

1、如果定位到的数组位置没有元素 就直接插入。
2、如果定位到的数组位置有元素,遍历以这个元素为头结点的链表,依次和链表上的 key 比较,如果存在key 相同的节点就直接覆盖,没有相同的节点就采用头插法将元素插入链表。与JDK1.8链表插入元素的不同点就在于1.8是尾插法,1.7是头插法
img

首先贴一下JDK1.7中HashMap成员属性与1.8相比不同的两个,在put()源码中会出现

//HashMap内部的存储结构是一个数组,此处数组为空,即没有初始化之前的状态  
static final Entry<?,?>[] EMPTY_TABLE = {};  
//空的存储实体  table是真正存储元素的数组
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; 

put()方法源码:

/**
 * 将“key-value”添加到HashMap中 
 * @return 如果插入的key在HashMap中已存在,将新插入的value替换旧value,并且返回旧value
 *         如何插入的key在HashMap中不存在,则返回Null
 */
public V put(K key, V value) {
	// 1. 若 哈希表未初始化(即 table为空) 
	// 则使用构造函数进行初始化 数组table  
    if (table == EMPTY_TABLE) {
        // 分配数组空间
        // 入参为threshold,此时threshold为initialCapacity initialCapacity可以是构造方法中传入的大小,如果构造方法没有指定HashMap容量大小,则使用默认值1<<4(=16)
        inflateTable(threshold);
    }
    
	// 2. 判断key是否为空值null
	// 2.1 若key == null,则将该键-值 存放到数组table 中的第1个位置,即table [0]
	// (本质:key = Null时,hash值 = 0,故存放到table[0]中)
	// 该位置永远只有1个value,新传进来的value会覆盖旧的value
    if (key == null)
        return putForNullKey(value);
    
	// 2.2 若 key ≠ null,则计算存放数组 table 中的位置(下标、索引)
	// a. 根据键值key计算hash值
    int hash = hash(key);    
	// b. 根据hash值 最终获得 key对应存放的数组Table中位置
    int i = indexFor(hash, table.length);
 
    
	// 3. 判断该key对应的值是否已存在(通过遍历 以该数组元素为头结点的链表 逐个判断)
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
	      // 3.1 若该key已存在(即 key-value已存在 ),则用新value替换旧value,并返回旧value
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            // 调用value的回调函数,该函数为空实现
            e.recordAccess(this);
            return oldValue;
        }
    }
    // 结构性修改,记录HashMap被修改的次数。保证并发访问时,若HashMap内部结构发生变化,快速响应失败
    modCount++;
       // 3.2 若 该key不存在,则将“key-value”添加到table中
    addEntry(hash, key, value, i);
    return null;
}

1)、初始化哈希表

真正初始化哈希表(初始化存储数组table)是在第1次添加键值对时,即第1次调用put()时,而不是在构造函数中。

inflateTable()方法用于初始化HashMap,即初始化数组(table)、扩容阈值(threshold)。

​ ****inflateTable的源码如下:

/**
 * 初始化hash表
 * @param toSize 指定HashMap容量大小
 */
private void inflateTable(int toSize) {
    // 1. capacity必须是2的次幂,将传入的容量大小toSize转化为:大于传入容量大小toSize的最小的2的次幂
    // 即如果传入的是容量大小是19,那么转化后,初始化容量大小为32(即2的5次幂)
    int capacity = roundUpToPowerOf2(toSize);
    // 2. 重新计算阈值 threshold = 容量 * 加载因子  
    // 取capacity*loadFactor和MAXIMUM_CAPACITY+1的最小值,capaticy一定不会超过MAXIMUM_CAPACITY,除非loadFactor大于1
    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    // 3. 使用计算后的初始容量(已经是2的次幂) 为table分配空间,即初始化数组table(作为数组长度)
    // 即 哈希表的容量大小 = 数组大小(长度)
    table = new Entry[capacity];
    // 选择合适的Hash因子(即Hash种子),好的Hash种子能提高计算Hash时结果的散列性
    initHashSeedAsNeeded(capacity);
}

inflateTable这个方法用于为主干数组table在内存中分配存储空间,通过roundUpToPowerOf2(toSize)可以确保capacity为大于或等于toSize的最接近toSize的二次幂,比如toSize=13,则capacity=16;to_size=16,capacity=16;to_size=17,capacity=32。其实现如下:

/**
 * 找到大于传入容量大小的最小的2的次幂
 */
private static int roundUpToPowerOf2(int number) {  
//若 容量超过了最大值,初始化容量设置为最大值 ;否则,设置为:大于传入容量大小的最小的2的次幂
return number >= MAXIMUM_CAPACITY  ? 
    MAXIMUM_CAPACITY  : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
} 

roundUpToPowerOf2中的这段处理使得数组长度一定为2的次幂,Integer.highestOneBit是用来获取最左边的bit(其他bit位为0)所代表的数值。

2)、当 key ==null时,将该 key-value 的存储位置规定为数组table 中的第1个位置,即table [0]

/**
 * 将key为null的value值放入table[0]上
 */
private V putForNullKey(V value) {  
    // 遍历以table[0]为首的链表,寻找是否存在key==null 对应的键值对
    // 1. 若有:则用新value 替换 旧value;同时返回旧的value值
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {  
        if (e.key == null) {   
            V oldValue = e.value;  
            e.value = value;  
            e.recordAccess(this);  
            return oldValue;  
        }  
    } 
}

从此处可以看出:

  • HashMap的键key 可为null(区别于 HashTable的key 不可为null)
  • HashMap的键key 可为null且只能为1个,但值value可为null且为多个

3)、当key≠null的时候,计算key的Hash并根据Hash值计算对应在able中的下标

hash()方法:

/**
 * 源码分析1:hash(key)
 * 该函数在JDK 1.7 和 1.8 中的实现不同,但原理一样 = 扰动函数 = 使得根据key生成的哈希码(hash值)分布更加均匀、更具备随机性,避免出现hash值冲突(即指不同key但生成同1个hash值)
 * JDK 1.7 做了9次扰动处理 = 4次位运算 + 5次异或运算
 * JDK 1.8 简化了扰动函数 = 只做了2次扰动 = 1次位运算 + 1次异或运算
 */
// JDK 1.7实现:将 键key 转换成 哈希码(hash值)操作  = 使用hashCode() + 4次位运算 + 5次异或运算(9次扰动)
static final int hash(int h) {
    h ^= k.hashCode(); 
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

通过hash函数得到散列值后,再通过indexFor进一步处理来获取实际的存储位置,其实现如下:

 /**
 * 函数源码分析2:indexFor(hash, table.length)
 * JDK 1.8中实际上无该函数,但原理相同,即具备类似作用的函数
 */
static int indexFor(int h, int length) {  
    // 将对哈希码扰动处理后的结果 与运算(&) (数组长度-1),最终得到存储在数组table的位置(即数组下标、索引)
    return h & (length-1); 
}

4)、当key≠null时,得到存储的下标位置后,我们就可以将元素放入HashMap中

先判断链表中是否已经存在与要插入元素的key相同的元素,如果有,直接用要插入的新value覆盖旧value。如果没有,则调用addEntry()方法将元素插入:

/**
 * 添加链表元素   
 * 作用:添加键值对(Entry )到 HashMap中
 * @param bucketIndex 元素要插入到数组table的索引位置(下标)
 */
void addEntry(int hash, K key, V value, int bucketIndex) {  
    // 1. 插入前,先判断容量是否足够
    // 1.1 若不足够,则进行扩容(2倍)、重新计算Hash值、重新计算存储数组下标
    if ((size >= threshold) && (null != table[bucketIndex])) {  
        resize(2 * table.length); // a. 扩容2倍
        hash = (null != key) ? hash(key) : 0;  // b. 重新计算该Key对应的hash值
        bucketIndex = indexFor(hash, table.length);  // c. 重新计算该Key对应的hash值的存储数组下标位置
    }  
    // 1.2 若容量足够,则创建1个新的数组元素(Entry) 并放入到数组中
    createEntry(hash, key, value, bucketIndex);  
}  
 
 
/**
 * 创建元素,并将新元素添加到HashMap中 
 * 作用: 若容量足够,则创建1个新的数组元素(Entry) 并放入到数组中
 */  
void createEntry(int hash, K key, V value, int bucketIndex) { 
    // 1. 把table中该位置原来的Entry保存  
    Entry<K,V> e = table[bucketIndex];
    // 2. 使用头插法讲元素插入到链表中,新元素成为链表头节点,新元素的next节点为原链表头节点。这保证了新插入的元素总是在链表的头  
    table[bucketIndex] = new Entry<>(hash, key, value, e);  
    // 3. 哈希表的键值对数量计数增加
    size++;  
} 

4、JDk1.7和JDK1.8的区别

img

posted @ 2022-11-08 18:30  RitoQ  阅读(152)  评论(0)    收藏  举报