JDK1.8HashMap源码分析与总结

249 阅读18分钟

前言

通过学习jdk1.7版本的HashMap源码我们知道,jdk1.7中HashMap的transfer方法在并发条件下容易产生死循环链表的问题,并且 在某些情况下,一条链表下挂载着许多节点,查找效率很低。此外,HashMap的put方法采取头插法的方式将新的节点插在链表头部,这使得HashMap扩容后发生链表元素逆序。在jdk1.8版本中,java开发团队对HashMap进行了一系列优化,提高了HashMap的性能。

在jdk1.8中,HashMap底层实现采用的是数组+链表+红黑树的数据结构,在链表长度达到一定的阈值时,可能会将链表转换成红黑树,将查找操作的时间复杂度由O(N)优化到O(logN)。其put方法将节点插入链表时改换成尾插法,在扩容后链表元素依然保持原来的顺序。

为什么采用红黑树?既然红黑树是一种平衡二叉树,那为什么不使用AVL树?

因为:使用红黑树不仅可以提高HashMap的性能,而且可以作为HashMap处理哈希冲突的另一种策略。jdk1.7版本的HashMap当某条链表中的元素数量太多时,增删改查效率低,HashMap性能不高。而红黑树的优点是增删改查效率较高,引入红黑树这种数据结构后,可以有效提高性能。当链表的长度到达一定值时,可以将链表转换成红黑树(链表与红黑树的转换条件见后面详细描述)。同时,当发生哈希冲突时,若当前table[index]位置是一颗红黑树,那么可以通过将该节点插入到红黑树来解决。

红黑树实际上不是”完全平衡“的二叉树,红黑树相比AVL树最多不平衡一层,相当于查询的时候最多只比AVL树多一次比较,但是红黑树增删节点性能较高,红黑树增加和删除节点时不一定像AVL树一样经过多次旋转以及平衡计算来维持平衡二叉树。

jdk1.8版本下的HashMap依然是线程不安全的,在高并发场景中推荐使用java.util.concurrent包下的ConcurrentHashMap来保证线程安全。

底层实现

HashMap底层实现

HashMap节点的具体实现

//普通节点,和jdk1.7HashMap中的Entry是一样的 
static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
 }
//树节点,TreeNode--->LinkedHashMap.Entry<K,V>------>HashMap.Node<K,V>
//由此可见,树节点和普通节点差不多,但是树节点占用的空间比普通节点大很多
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;
}

成员变量/常量

==大部分与jdk1.7中的相同==

	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;

    /*
        将链表转化为红黑树的阈值
        面试题:为什么这个阈值是8?为什么不是7,或者20?
        参考源码中的注释:
        Because TreeNodes are about twice the size of regular nodes, we
        use them only when bins contain enough nodes to warrant use
        (see TREEIFY_THRESHOLD). And when they become too small (due to
        removal or resizing) they are converted back to plain bins.  In
        usages with well-distributed user hashCodes, tree bins are
        rarely used.  Ideally, under random hashCodes, the frequency of
        nodes in bins follows a Poisson distribution
        (http://en.wikipedia.org/wiki/Poisson_distribution) with a
        parameter of about 0.5 on average for the default resizing
        threshold of 0.75, although with a large variance because of
        resizing granularity. Ignoring variance, the expected
        occurrences of list size k are (exp(-0.5)  pow(0.5, k) /
        factorial(k)). The first values are:
     
        0:    0.60653066
        1:    0.30326533
        2:    0.07581633
        3:    0.01263606
        4:    0.00157952
        5:    0.00015795
        6:    0.00001316
        7:    0.00000094
        8:    0.00000006
        more: less than 1 in ten million
        
解释:
一个红黑树节点的大小比普通链表节点大,所以要尽量在链表的长度达到一定值时,才将其转化为红黑树.但是,当HashMap进行remove或resize操作使得红黑树中节点的数目变小时,又会将红黑树重新转换成链表.所以,在使用散列性较好的hash算法时,应该要较少使用红黑树.
根据概率学研究,在理想情况下经过哈希散列,table数组每一个桶中的节点个数遵循参数为0.5的泊松分布.
可以发现:桶中存在8个节点的概率为0.00000006,而大于8个的概率更是小.
如果阈值设为7,树节点占用空间比普通节点大,红黑树转化为链表的操作太频繁;如果设置为20,链表太长,链表性能低;
所以,链表转化为红黑树的阈值为8.

    */
    static final int TREEIFY_THRESHOLD = 8;

    /*
        将红黑树转换为链表的阈值
        
        为什么为6,而不是7或8?
        因为:链表与红黑树之间的转换十分复杂,应该尽量避免频繁的相互转换.
        当链表长度大于8时可能就会转换成红黑树了,若红黑树转换成链表的阈值太接近,就会造成红黑树与链表频繁的转换.
    */
    static final int UNTREEIFY_THRESHOLD = 6;

    /*
        链表转换为红黑树所要求的数组table长度的阈值
        
        注意:链表转换成红黑树的条件有两个:
        1、链表的长度大于8
        2、数组的长度大于等于64
        如果条件2不满足,则会对HashMap进行扩容(见resize方法)
    */
    static final int MIN_TREEIFY_CAPACITY = 64;
    //HashMap中的数组
    transient Node<K,V>[] table;
    
    transient Set<Map.Entry<K,V>> entrySet;
    
    transient int size;
    
    transient int modCount;
    /*
    	负载因子,与扩容阈值相关,threshold=capacity*loadfactor
    	负载因子太小,查找性能高,空间利用率低,哈希冲突可能性小,扩容操作频繁
    	负载因子太大,空间利用率高,链表元素多,哈希冲突可能性高,查询效率低
    	默认值0.75是在 查找效率 和 空间利用率 之间均衡之后得到的结果
    	所以一般不手动设置负载因子,采用默认的0.75
    */
    final float loadFactor;
    /*
        HashMap扩容阈值
        **注意:jdk1.7和jdk1.8中HashMap的扩容条件不一样
        jdk1.8中只要HashMap元素个数size大于threshold,就需要进行扩容.**
    */
    int threshold;
    

方法

构造方法
 /*
        在HashMap中会为我们创建一个长度为大于initialCapacity的最小2的整数次幂的数组
        如果传入的initialCapacity不合适,可能导致扩容操作很频繁
        在《阿里巴巴Java开发手册》中建议:
        当我们明确HashMap中要存储的元素个数时,initialCapacity可以设为:
            initialCapacity = (需要存储元素个数/负载因子) + 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;
        //根据传入的initialCapacity,求得一个比它大的 最小2的整数次幂
        /*
            为了防止为创建数组分配了内存空间,但是HashMap没有被使用的浪费空间的情况
            HashMap采取的是延迟加载机制
            构造方法中并未创建一个长度为2的整数次幂的数组
            而是将这个2的整数次幂先暂时存在threshold中
            在第一次进行put操作时才会创建数组
         */
        this.threshold = tableSizeFor(initialCapacity);
    }

	public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
	//注意无参构造函数并没有在内部调用具有两个参数的构造方法
	public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
tableSizeFor方法

求大于给定cap的最小2的整数次幂

//该方法也是获得一个数count,该数符合:
    //  1、count = 2的幂次方.
    //  2、count >= iniCapacity
    //见jdk1.7HashMap中的roundUpToPowerOf2方法进行比较
    static final int tableSizeFor(int cap) {
        //如果不-1的话,就会发生:传入的8,却返回了16的情况
        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;
    }
hash方法

根据key计算hash值

static final int hash(Object key) {
        int h;
        /*
            将hashcode值h与其高16位进行按位异或运算

            如果数组长n很小,用hash值和n-1进行按位与操作时,求得的index就会只与hash值低位有关

            如果hash值的高位变化很大,低位变化很小,就会很容易发生hash冲突
            
            该hash算法将hashcode的高位利用起来,可以避免这种情况
         */
         /*
           经过hash算法返回的hash值会用于与tab.len-1进行运算从而计算元素放在数组位置的下标 
           
           在hash算法中,jdk1.8的hash方法进行了两次扰动处理(右移一次、异或一次)
           
           扰动处理的目的是使得计算出的hash值低位尽可能地随机、均匀,从而可以减少hash冲突的概率
         */
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
put方法(重要)

向HashMap添加节点。如果存在key相同的节点,则用传入的value覆盖oldValue,并返回oldValue

如果不存在,则插入节点,返回null。

put方法流程:

1、判断table是否被初始化,没有则调用resize方法初始化

2、根据index=hash&(table.len-1),求得index

3、是否发生哈希冲突

​ 3.1没有发生哈希冲突,即table[index]==null,则table[index]=newNode(hash,key,value,null)

​ 3.2发生了哈希冲突,即table[index]!=null(下述步骤也就是查找有没有key相同的节点,存在则返回该节点)

​ 3.2.1根据hash和key,判断插入节点是不是table[index],是则e=table[index]

​ 3.2.1如果插入节点不是table[index]

​ 3.2.1.1如果table[index]是一颗红黑树,红黑树解决哈希冲突,e=putTreeVal(hash)

​ 3.2.1.2如果table[index]是一条链表,链表解决哈希冲突

​ 3.2.1.2.1如果链表中存在key与插入节点key相同的节点,e=node

​ 3.2.1.2.2如果不存在,该节点插到链表尾部,如果链表长度>8,则treeifyBin

​ (treeifyBin方法可能进行扩容,可能进行链表转换红黑树)

4、table[index]中是否存在key与传入的key相同的节点(e==null?),存在则使用传入的value覆盖oldVal并返回oldVal

5、不存在则判断HashMap中节点数目是否大于threshold,是则进行扩容

==详细过程阅读下段代码:==

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    
    /**
     * Implements Map.put and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent 如果该参数为ture,那么已存在的key相同的节点旧值不会被覆盖
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    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
        if ((tab = table) == null || (n = tab.length) == 0)
            //第一次进行put方法时,才创建数组!!!!
            n = (tab = resize()).length;
        
        //table[i]为空,没有发生哈希冲突,直接将节点放在table[i]位置
        
        /*
        	i = (n - 1) & hash,n为table的length
        	为什么HashMap要求table的长度必须为2的整数次幂?
        	因为:
        	在求插入节点放入数组位置的下标时,是用hash与length-1进行按位与运算
        	如果length不为2的整数次幂,
        	则length-1如下:
        		00..00 x..x0x..x
        	则经过按位与后
        		00..00 x..x1x..x对应的一些位置永远不会被访问到
        		既浪费了空间,也增加了哈希冲突的可能性
        */
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        //table[i]不为空,发生了哈希冲突
        else {
            //e代表原table[i]中与传入的hash和key都相等的node
           	//如果e为null,则返回的oldValue也为null
            Node<K,V> e; K k;
            //插入的node与table[i]的头结点hash、key都相等
            /*
            	(p.hash==hash)&&((p.key==key)||(key!=null&&key.equals(p.key)))
            	此处延伸出三个要点:
            	1、如果HashMap的Key为Object类型,那么需要重写equals方法和hashCode方法
            	2、向HashMap中put K-V时,尽量用被final修饰的变量作为Key
            		因为如果不是final修饰,当K-Vput进HashMap并且K改变时,
            		再进行get(K)就取不到之前的Node,返回的是null
            	3、java基本类型变量无法作为HashMap的Key,而对应的包装类可以
            		因为String、Integer、Character等是被final修饰的类,并且重写了hashCode和equals方法
            */
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //table[i]是一颗红黑树,将问题转化为将node插入到红黑树中
            //红黑树解决哈希冲突
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            //table[i]是一条链表
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        /*
                            此处可见,jdk1.8中插入链表时采取的尾插法
                         */
                        p.next = newNode(hash, key, value, null);
                        //若链表长度大于8,则将链表转换成红黑树
                        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;
                }
            }
            //返回oldValue
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        //如果e不为空直接返回了oldValue,HashMap中节点数目不变
        //modCount不需要修改
        ++modCount;
        /*
        	注意:put方法
        	1.7中是先扩容再添加元素
        	1.8中是先添加元素再扩容
        	
        	并且扩容的条件也不同
        	1.7中要满足1、节点个数达到扩容阈值;2、发生哈希冲突
        	而1.8中只需要满足条件1就进行扩容
        */
        if (++size > threshold)
            //扩容
            resize();
        afterNodeInsertion(evict);
        return null;
    }
resize方法(重要)

初始化HashMap的table或者对table进行扩容操作。

如果是扩容,则会将原table中的元素移动到新的table中,并且链表中元素的顺序不变

1、计算新的newThreshold、newCapacity,并创建新的table,newTab=new Node[newCapacity]

(2倍扩容,即newCapacity=2*oldCapacity)

2、如果原table不为空,即不是进行初始化,则遍历原tab中的每一个链表或红黑树tab[i]

3、如果tab[i]不为空,tab[i]只有一个元素,则newTab[hash&(newCapacity-1)]=tab[i]

​ 如果tab[i]为红黑树,则该红黑树进行split操作,可能会将红黑树转换成链表

​ 如果tab[i]为链表,则遍历该链表中的节点,创建两个临时链表hiNode、loNode分别用于保存放在 newTab[i+oldCapacity]和newTab[i]中的节点,最后将hiNode、loNode放在newTab中对应位置

==详细过程阅读下段代码:==

 /**
     * 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.
     *
     * @return the table
     */
    //初始化或扩容table
    final Node<K,V>[] resize() {
        /**
         * 如果创建hashMap时调用的是无参构造函数
         *  table = null
         *  threshold = null
         *  loadfactor = 0.75
         *
         * 如果调用的是有参构造函数(initcapacity或initcapacity和loadfactor)
         *  table = null
         *  threshold >= initcapacity
         *  loadfactor != null
         */
        Node<K,V>[] oldTab = table;
        //初始化table的话oldCap必定=0
        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
        }
        //创建hashMap时指定了参数,在构造函数中就为threshold赋予了一个大于等于initCapacity的2次幂
        //这个2次幂就是初始化table时table的length
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        //创建hashMap时没有指定参数
        //该情况下的initCapacity与loadfactor都是采用的默认值
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        //创建hashMap时指定初始容量的情况
        //或扩容的情况
        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"})
            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) {
                    //方便GC下次回收
                    oldTab[j] = null;
                    //如果oldTab[j]只有一个节点
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    //如果oldTab[j]是一颗红黑树
                    else if (e instanceof TreeNode)
                        //对红黑树进行split操作,可能会发生将红黑树转换为链表的操作
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    //oldTab[j]是一条链表
                    /**
                     * 将table扩容为长度为2*oldTable.length的newTable后
                     * 原table[j]中的节点,在table扩容后,要么在j,要么在j+oldTable.length
                     *
                     * 假设原table长度为    000..000 100..00
                     * oldTab.length-1==> 000..000 011..11
                     * hash:              xxx..xxx Xxx..xx
                     *                  &
                     *
                     *                    000..000 0xx..xx
                     *                    即为节点在数组中的下标index
                     * 扩容后table长度为    000..001 000..00
                     * newTab.length-1==> 000..000 111..11
                     * hash:              xxx..xxx Xxx..xx
                     *                  &
                     *
                     *                    000..000 Xxx..xx
                     *         =          000..000 0xx..xx
                     *                  + 000..000 X00..00
                     *                    即为节点在扩容后的数组中的下标newIndex
                     * index与newIndex的区别在于这个大X:X=1时,newIndex = index+oldTab.length
                     *                               X=0时,newIndex = index
                     *   用000..000 X00..00和oldTab.length进行与操作,就可以判断X是0还是1
                     */
                    else { // preserve order --->由此可知,1.8版本的hashMap扩容后链表元素的顺序不变,而1.7中会发生反序

                        /**
                         * jdk1.8的hashMap插入是用的尾插法,1.7中是头插法
                         */

                        //loHead、loTail,lo代表low
                        //这两个指针代表将要放入到newTab[j]中的链表的头结点和尾节点
                        Node<K,V> loHead = null, loTail = null;
                        //hiHead、hiTail,hi代表high
                        //这两个指针代表将要放入到newTab[j+oldTab.length]中的链表的头结点和尾节点
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            //此处是用oldCap进行&操作,而不是oldCap-1
                            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);
                        //将链表放到tab的相应位置
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }
treeifyBin方法

将链表转换成红黑树,但并非一定会发生转换,也可能会进行扩容

重点关注判断条件

final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        //如果此时数组的长度大于等于64了,才会将链表转换成红黑树,否则对hashMap扩容
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }

==get、remove、replace方法比较简单,不过多解释,放在此处是方便以后查阅==

get方法

containsKey方法底层也是调用的getNode方法,根据getNode方法的返回值是否为空来返回false/true

public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

     /**
     * Implements Map.get and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @return the node, or null if none
     */
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }
remove方法

使用该方法时,注意remove方法的返回值

public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }
    
    @Override
    public boolean remove(Object key, Object value) {
        return removeNode(hash(key), key, value, true, true) != null;
    }
    
    /**
     * Implements Map.remove and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to match if matchValue, else ignored
     * @param matchValue if true only remove if value is equal
     * @param movable if false do not move other nodes while removing
     * @return the node, or null if none
     */
    final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            Node<K,V> node = null, e; K k; V v;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            else if ((e = p.next) != null) {
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            //前面步骤与getNode方法类似
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                else if (node == p)
                    tab[index] = node.next;
                else
                    p.next = node.next;
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }
replace方法
 @Override
    public boolean replace(K key, V oldValue, V newValue) {
        Node<K,V> e; V v;
        if ((e = getNode(hash(key), key)) != null &&
            ((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {
            e.value = newValue;
            afterNodeAccess(e);
            return true;
        }
        return false;
    }

    @Override
    public V replace(K key, V value) {
        Node<K,V> e;
        if ((e = getNode(hash(key), key)) != null) {
            V oldValue = e.value;
            e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
        return null;
    }

总结

​ jdk1.7与jdk1.8中HashMap的区别

​ 底层数据结构实现不同:

​ JDK1.7采用的数组+链表

​ JDK1.8采用的数组+链表+红黑树

​ put方法将节点插入链表的策略不同:

​ JDK1.7头插法

​ JDK1.8尾插法

​ put方法中插入节点和扩容的顺序不同:

​ JDK1.7先扩容再插入节点

​ JDK1.8先插入节点再扩容

​ Hash算法不同:

​ JDK1.7进行了9次扰动处理(4次位运算+5次异或)

​ JDK1.8进行了2次扰动处理(1次位运算+1次异或)

​ HashMap解决Hash冲突

该图转载于blog.csdn.net/qq_36520235… ,侵权删。

HashMap解决Hash冲突