HashMap类源码详解

一、简介

1.存储结构及特点

HashMap 底层的数据结构:
底层是利用邻接表(散列表)的形式进行存储的:数组 + 链表/红黑树。

HashMap 中每个节点 的存储方式为<key,value>形式,
其中,key 无序不可重复,value可以重复。

2.存储结构的由来

首先我们需要明白:

· 当hash值不同时,对象一定不同。

原因:因为用同样的计算方法计算同样的东西,计算结果一定相同。结果不同,就说明他俩根本不是同一个东西。

· 当hash值相同时,对象可能相同,也可能不同。

原因:因为不同的东西经过相同的计算方式,结果可能会相同。

所以,此时还需要借助equals()方法进行内容的具体判断。

也就是说,用hashCode()方法计算对象的存储位置时,不同的对象是可以产生相同的hash值的。
因此,当同一个hash值对应的对象不止有一个时,就将相同地址的对象用链表结构连接起来。

3.邻接表的存储方式

HashMap中底层的数组部分,是可以通过key计算出来的hash值直接进行快速访问的。

数组内存储的内容是Entry类对象,这个类有三个组成部分:key+value(键值对),next(指向下一个Entry类对象的指针)。

后面链表中的每个节点,存的也是一个 由 key+value(键值对),以及指向下一个节点地址的next指针包装成的一个Entry对象。

4.HashMap的存储过程

在往HashMap中put节点(键值对)时,首先通过底层重写的散列算法hashCode()方法计算出key的hash值,从而确定要将当前节点插入到数组中的哪个位置。要将当前节点定位到散列表的相应位置,所以需要重写hashcode()方法。

但是,当两个key计算出来的hash值相同时,则后来的元素就要添加到先来的元素的后面(JDK1.8采用尾插法),从而形成链表。所以,同一个链表上的Hash值是相同的(在jdk1.8中,如果链表的长度>8,则此链表就会转化成红黑树,从而提高查找效率)。然后依次遍历该位置后面链表中的所有元素,此时就需要调用该类重写的equals()方法进行内容的具体比较了。相等则修改key对应的value值,不等则添加该节点。

实际上,HashMap中判断元素是否重复是有两个过程的,首先生成即将插入的新对象其key的hash值。当我们发现没有相同的散列值时,就可以判断这是一个不可能重复的元素,就可以直接插入了。当我们发现table中已经有相同的散列值时,并不能直接判断此对象就是个重复元素(因为不同的对象也可能产生相同的hash值),此时还需要调用equals()方法对具体内容再判断一次,如果判断的结果还是真,就表示这真的是个重复元素,就不做存入处理,只做值value的更新处理;如果equals判断为假,那么就重新比较其他的节点,再进行插入判断。

5.HashMap中,需要重写equals()和hashCode()的原因

Object中的原生方法:

  • hashCode() :返回的是对象的地址引用。

    所以,在这种情况下,不同对象的hash值肯定不同,即使两个对象的内容完全一致,但由于地址引用不同,所以hash值也就不同。

  • equals() :底层默认的比较方式是"=="号。

    所以,对于引用数据类型来说,比较的是对象的地址(8个基本数据类型+String类型进行值比较)。

equals()方法和hashCode()方法都属于Object类,在Java中,所有的类都是Object类的子类,也就是说,任何Java对象都可调用Object类的方法。

很明显,Object类原生的equals()方法就是用来判断,两个对象是否是同一个地址引用的。在Object类源码中,其底层是使用了“==”来实现比较的。也就是说,通过比较两个对象的内存地址是否相同,来判断这两个对象是否是同一个对象。但实际上,该equals()方法通常没有什么太大的实用价值。我们往往需要用equals()方法来判断, 2个对象在逻辑上是否等价,而非验证它们地址内存的唯一性。这样我们在实现一些自己的类时,就需要重写equals()方法。

而在HashMap中,需要通过重写hashCode()方法,重新定义计算key的hash值的计算方式(根据key的实际内容进行计算,而不是返回对象的地址引用)。由于内容不同的对象也可能被计算出相同的hash值,所以此时就需要通过重写equals()方法进行对象的内容判断(内容相同的对象被认为是同一个对象,返回true。而不是equals()利用原来的"=="号,来比较引用数据类型的地址是否相同。)。

6.JDK1.7和1.8的区别

1>.存储结构:

JDK1.7中,底层的存储结构是:数组+链表。

而JDK1.8中:数组+链表/红黑树(链表长度>8时,会由链表转换为红黑树以提高效率)。

思考:

红黑树是为了解决链表查询出现O(n)情况,那么为什么不用其他树呢?

如平衡二叉树等,我们通过以下二方面分析:

​ 平均插入效率:链表>红黑树>平衡二叉树

​ 平均查询效率:平衡二叉树>红黑树>链表

可以看出红黑树介于二者之间,hashMap作为各种操作频繁的容器,自然选择综合性能较好的红黑树

2>.新节点插入链表的方式:

JDK1.7中用的是头插法(扩容后与原位置相反,因为resize()会导致环形链表)。

JDK1.8用的是尾插法(因为1.8中引入了红黑树结构,要通过遍历链表,将链表转换为红黑树),扩容后位置与原链表相同。

3>.hash算法简化:

key的Hash值的计算区别

JDK1.7:

h^ =(h>>>20)^(h>>>12)  
return h ^(h>>>7) ^(h>>>4);

JDK1.8:

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

JDK1.7中因为要保持hash函数的散列性,所以进行了多次的异或等位运算。

JDK1.8因为链表长度超过8会转为红黑树,所以我们可以稍微减少元素的散列性,从而避免多次进行异或等位运算。

4>.扩容机制:

JDK1.7扩容条件:元素个数 > 容量(16) * 加载因子 (0.75) && 插入的数组位置有元素存在。
JDK1.8扩容条件 :元素个数 > 容量 (16) * 加载因子(0.75)。

虽然都是进行2倍扩容,但是JDK1.7在扩容的时候,会重新计算位置。

JDk1.8则不会,只需关注新增的参与hash运算最高的那个bit位是1还是0就好了。

是0的话,扩容前后索引下标位置不变,新索引 = 原索引。

是1的话,新索引 = 原索引+ oldCap(旧数组长度)。

7.方法简介

1>.基本方法

1.增加( put(key,value) )
2.删除( remove(key) )
3.更改(replace(key,value))

(其实,先put(key,value1),后又put(key,value2),value2覆盖了value1,就是发生了更改。)

4.查找(get(key))

2>.重要方法

1.keySet()方法:

可以通过 keySet()方法,获取 HashMap中所有的key,此方法的返回值是Set类型。
例:Set<key.type> keys = hashMap.keySet();

2.entrySet()方法:

可以通过 entrySet()方法,获取HashMap中所有的Entry对象,每个Entry对象的信息包括(key + value + nextNode)。
例:Set< Map.Entry <key.type,value.type> > entrys = hashMap.entrySet()

3>.特殊方法

1.getOrDefault(key , defaultValue)方法:

· 如果key存在,就返回其对应的value值;
· 如果key不存在,就返回默认值defaultValue(以前返回的是null)。

2.putIfAbsent(key , value)方法:

· 当key不存在时,向HashMap中添加此键值对;
· 如果此key存在,则不发生任何更改(以前用put()方法存放时,value值会发生改变)。

二、基础信息

1.继承的父类&实现的接口

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {

2.重要属性

 	//允许序列化和反序列化
    private static final long serialVersionUID = 362498820763181265L;

    /**
     * The default initial capacity - MUST be a power of two.
     * 默认的初始容量-必须是2的整数次幂。
     *
     * 00001 << 4 = 10000 = 2^4 = 16,
     * 所以,table的默认初始容量 DEFAULT_INITIAL_CAPACITY = 1 << 4 = 16
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    /**
     * The maximum capacity, used if a higher value is implicitly specified
     * by either of the constructors with arguments.
     * MUST be a power of two <= 1<<30.
     *
     * 最大容量,当两个构造函数中任何一个带参数的函数隐式指定较大的值时使用。
     * 最大容量一定是2 <= 1<<30的幂。
     * 即table的最大容量为2^30
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * The load factor used when none specified in constructor.
     * 在构造函数中未指定时使用的默认负载系数。
     *
     * 加载因子用于表示Hash表中,元素的填满程度。
     * 加载因子越大,表内填满的元素就越多,空间利用率就越高,但冲突的机会就会加大。
     * 加载因子越小,填满的元素越少,冲突的机会就会减小,但空间的浪费就会增多。
     * 而此处,加载因子的默认值是0.75
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * The bin count threshold for using a tree rather than list for a
     * bin.  Bins are converted to trees when adding an element to a
     * bin with at least this many nodes. The value must be greater
     * than 2 and should be at least 8 to mesh with assumptions in
     * tree removal about conversion back to plain bins upon
     * shrinkage.
     *
     * 使用树而不是列表的容器计数阈值。
     * 当向至少有这么多节点的容器中添加元素时,容器被转换为树。
     * 该值必须大于2,并且应该至少为8,
     * 以与关于在收缩时转换回普通bins的树移除假设相匹配。
     *
     * 桶的树化阈值:即 链表转成红黑树的临界值。
     * 在存储数据时,当链表长度 > 8时,则将链表转换成红黑树
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * The bin count threshold for untreeifying a (split) bin during a
     * resize operation. Should be less than TREEIFY_THRESHOLD, and at
     * most 6 to mesh with shrinkage detection under removal.
     *
     * 在调整大小操作期间取消(分割)存储库的存储库计数阈值。
     * 应小于TREEIFY_THRESHOLD,且最多为6,以去除收缩检测下的网格。
     *
     * 桶的链表还原阈值:即 红黑树转为链表的阈值。
     * 当在扩容(resize())时(此时HashMap的数据存储位置会重新计算),
     * 在重新计算存储位置后,如果原有的红黑树内的节点数量 < 6时,则将 红黑树转换成链表。
     */
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     * The smallest table capacity for which bins may be treeified.
     * (Otherwise the table is resized if too many nodes in a bin.)
     * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
     * between resizing and treeification thresholds.
     *
     * 可以树形化容器的最小表容量。
     * 即 :当哈希表中的容量 > 64时,才允许树形化链表 .
     * (否则,如果一个容器中有太多节点,则会调整表的大小,而不是树形化链表。)
     * 为了避免 进行扩容 和 树形化 之间的选择冲突,
     * 这个值应该至少为4 * TREEIFY_THRESHOLD。
     *
     * 最小树形化容量阈值:
     * 当整个hashMap容器中的节点数>64时,才允许链表树化。
     */
    static final int MIN_TREEIFY_CAPACITY = 64;

/**
     * The table, initialized on first use, and resized as
     * necessary. When allocated, length is always a power of two.
     * (We also tolerate length zero in some operations to allow
     * bootstrapping mechanics that are currently not needed.)
     *
     * 该表在第一次使用时初始化,并根据需要调整大小。
     * 当分配时,长度总是2的幂。
     * (在某些操作中,我们也允许长度为0,以允许当前不需要的引导机制。)
     *
     * 这是邻接表的数组部分。
     * 同hash值的key被放在同一链表里。
     * 链表头是最新加入的数据元素,数组中的每个元素就是同hash值的链表头。
     */
    transient Node<K,V>[] table;

    /**
     * Holds cached entrySet(). Note that AbstractMap fields are used
     * for keySet() and values().
     * 保存缓存entrySet()。
     * 注意,keySet()和values()使用了AbstractMap字段。
     *
     * 存放具体数据节点的集合
     */
    transient Set<Map.Entry<K,V>> entrySet;

    /**
     * The number of key-value mappings contained in this map.
     * 此映射中包含的键-值映射的数量。
     * 用于记录元素个数
     */
    transient int size;

    /**
     * The number of times this HashMap has been structurally modified
     * Structural modifications are those that change the number of mappings in
     * the HashMap or otherwise modify its internal structure (e.g.,
     * rehash).  This field is used to make iterators on Collection-views of
     * the HashMap fail-fast.  (See ConcurrentModificationException).
     *
     * 当前 哈希表结构的修改次数(例如添加或删除元素时会被记录,但修改不会)
     */
    transient int modCount;

    /**
     * The next size value at which to resize (capacity * load factor).
     *
     * 要调整大小的下一个尺寸值(容量*负载系数)
     *
     * 扩容阈值:
     * 当哈希表中的元素个数size > 此阈值threshold时,会触发扩容操作。
     * 会重新设置长度(扩容阈值threshold = capacity(表长) * loadFactor(负载系数) ),
     * 表示符合装载因子的最大使用容量。
     *
     * @serial
     */
    int threshold;

    /**
     * The load factor for the hash table.
     * 哈希表的负载因子。
     *
     * @serial
     */
    final float loadFactor;

3.Node节点类

 	/**
     * Basic hash bin node, used for most entries.  (See below for
     * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
     *
     * 基本散列 bin节点,用于大多数条目。
     * (参见下面的treenode子类和LinkedHashMap的Entry子类。)
     */
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;//此节点的hash值
        final K key;//此节点的 键key
        V value;    //此节点的 值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;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        //计算当前节点hash值的hashCode()方法,重写自父类Object。
        //(借助父类Object 本地native修饰的hashCode()方法进行计算)
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        //修改当前节点的 value值,并将原来的旧值返回
        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        /**
         * 重写了父类Object的equals()方法:
         * 重新定义了比较规则:
         * 不再是Object中默认的,通过“==”来比较两个对象的地址引用了,
         * 而是比较属性的具体内容。
         */
        public final boolean equals(Object o) {
            //由于是引用数据类型,所以 "==" 比较的是变量的地址引用
            //如果 参数o 和 当前节点对象 的地址相同,表示是同一个对象
            if (o == this)
                return true;

            //首先判断 参数o的数据类型,如果数据类型和当前节点的数据类型相同
            if (o instanceof Map.Entry) {
                //将 形参o 类型转换为e
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;

                //如果当前的 key 和 形参e.getKey()的地址相同,
                //并且,当前的 value 和 形参e.getValue()的地址相同,则认为是同一个对象
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }

三、重要方法

1.构造函数

注意: 3个有参构造方法会给扩容阈值threshold赋值,而无参构造方法不会。

1>.无参构造

	/**
     * Constructs an empty <tt>HashMap</tt> with the default initial capacity
     * (16) and the default load factor (0.75).
     *
     * 构造一个空的HashMap,
     * 具有默认初始容量(16)和默认负载因子(0.75)。
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

2>.有参构造

1.HashMap(int initialCapacity, float loadFactor)方法:
 //构造一个空的HashMap,具有指定的初始容量和负载因子。
    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);

        //如果参数合法,分别给 负载因子loadFactor 和 扩容阈值threshold 赋值。
        this.loadFactor = loadFactor;
        //注意 扩容阈值threshold, 需要通过tableSizeFor()方法计算后给出(因为表的长度必须是2的整数幂)
        this.threshold = tableSizeFor(initialCapacity);
    }


 /**
     * Returns a power of two size for the given target capacity.
     *
     * 作用:返回一个大于等于参数cap的数,
     * 并且,这个数一定是2的整数幂。
     *
     * 位运算 >>> :表示无符号右移,左边补0
     * 逻辑运算 | : 逻辑"或"
     */
    static final int tableSizeFor(int cap) {//例:cap = 10
        int n = cap - 1;//n = 10-1 = 9 = 1001
        n |= n >>> 1;//n = 1001 | 0100 = 1101
        n |= n >>> 2;//n = 1101 | 0011 = 1111
        n |= n >>> 4;//n = 1111 | 0000 = 1111
        n |= n >>> 8;//n = 1111 | 0000 = 1111
        n |= n >>> 16;//n = 1111 = 15

        //n = 15 时,执行return n+1,最后返回10
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }
2.HashMap(int initialCapacity)方法:
	//构造一个空的HashMap,具有指定的初始容量和默认负载因子(0.75)。
    public HashMap(int initialCapacity) {
        //只传入初始容量时,第二个负载系数将传入默认值DEFAULT_LOAD_FACTOR = 0.75
        //然后利用上面的构造函数HashMap(int initialCapacity, float loadFactor)进行构造
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

3>.利用Map构造HashMap

HashMap(Map<? extends K, ? extends V> m) 方法:

/**
     * Constructs a new <tt>HashMap</tt> with the same mappings as the
     * specified <tt>Map</tt>.  The <tt>HashMap</tt> is created with
     * default load factor (0.75) and an initial capacity sufficient to
     * hold the mappings in the specified <tt>Map</tt>.
     * 
     * 使用与指定的Map相同的映射构造一个新的HashMap。
     * 创建HashMap时,默认负载因子(0.75)和初始容量足以容纳指定的Map中的映射。
     *
     * @param   m the map whose mappings are to be placed in this map
     * @throws  NullPointerException if the specified map is null
     */
    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();//记录形参m内的元素个数

        if (s > 0) {
            //如果 table 没有被初始化过
            if (table == null) { // pre-size
                //由于 扩容阈值threshold = capacity(表长) * loadFactor(负载系数)
                //所以先用ft大概算一下所需表长
                float ft = ((float)s / loadFactor) + 1.0F;
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
                //如果 计算得到的所需表长t > 扩容阈值
                if (t > threshold)
                    //则更新扩容阈值,通过tableSizeFor(t)得到一个不小于t的2的整数幂。
                    threshold = tableSizeFor(t);
            }
            //如果已初始化过,并且m的元素个数>扩容阈值
            else if (s > threshold)
                //则进行扩容处理
                resize();

            //复制数据,将m中的所有元素添加到hashMap中
            for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
                K key = e.getKey();
                V value = e.getValue();
                putVal(hash(key), key, value, false, evict);
            }
        }
    }

2.增加put(K key, V value)方法

    //增加:存入 键值对型 节点
    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 if true, don't change existing value(如果此处是true,已存在的key其对应的value将不会被更新)
     * @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) {

        //tab:用于存储当前散列表table的地址引用
        //p : 用于表示当前散列表中的节点元素
        //n : 用于表示散列表数组的长度
        //i : 用于表示计算后的最终下标。
        Node<K,V>[] tab; Node<K,V> p; int n, i;

        //如果 table为null,或者长度为0
        if ((tab = table) == null || (n = tab.length) == 0)
            //为哈希表做初始化处理
            //先为table扩容(扩容就是做了初始化),并用n记录扩容后表的长度
            n = (tab = resize()).length;

        //如果 利用表长n和hash值计算出来的下标i,在哈希表中对应的存储单元为null
        if ((p = tab[i = (n - 1) & hash]) == null)
            //就将此节点放到表table的对应位置
            tab[i] = newNode(hash, key, value, null);

        //否则表示,此hash值对应的节点不止一个
        else {
            //e : 用于记录与待插节点的hash值和key都完全一致的节点。
            Node<K,V> e; K k;
            //如果,数组中的节点p与新加节点的hash值相等
            //并且,键key也相同
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                //此时表示已经找到了重复元素,用e记录旧节点的地址引用,后续需要进行值替换
                e = p;

            //如果新加节点的key与数组中节点的key不同,
            //第一种可能是,去后面的树中去找(涉及红黑树)
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            //如果不去树中查询,就去链表中遍历。(至此已知,链表的头元素与待插元素的key不一致)
            else {
                //开始循环遍历链表
                for (int binCount = 0; ; ++binCount) {
                    //如果已经到达链表尾部,则表示此hash值对应的链表中没有与待插元素相同的key
                    if ((e = p.next) == null) {
                        //就把新节点添加到此链表的尾部,作为尾节点。
                        p.next = newNode(hash, key, value, null);
                        //如果添加完新节点后的链表长度>8,则由链表转换成红黑树。
                        // 为什么是长度>8?因为触发下面if语句的条件是:binCount >= 7。
                        // 由于binCount的初始值是0,
                        // 当binCount=7时,表示现在正在执行的是循环的第8次,
                        // 而且刚才又在链表的尾部添加了一个新节点,故此时链表长度是9,而9>8。
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);//将链表结构进行树化
                        break;
                    }
                    //如果在遍历链表的过程中,遇到与待插节点的hash值和key都相同的节点
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;//p = e = p.next,循环继续往后走的条件
                }
            }

            //若 e != null,则表示此hashMap中,存在与待插元素同hash且同key的重复元素,
            // 需执行value的更新操作。
            if (e != null) { // existing mapping for key
                V oldValue = e.value;//保存旧值
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;//更新value值
                afterNodeAccess(e);
                return oldValue;//并将旧值返回
            }
        }

        ++modCount;
        //如果 节点个数size > 扩容阈值threshold,则需执行扩容操作
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

3.删除remove()

1>. remove(Object key, Object value)方法:

	//删除节点:当key和value同时比对成功,才执行删除操作,否则不删。
    @Override
    public boolean remove(Object key, Object value) {
        return removeNode(hash(key), key, value, true, true) != null;
    }

2>. remove(Object key, Object value)方法:

	//删除:根据键key ,删除对应的节点对象
    public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }


	//删除节点操作的具体实现
    final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        //tab:保存当前table数组的地址引用
        //n : 记录当前table数组的长度
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        //如果 此hash值对应的桶中存在节点p
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            //node:用于记录查找结果
            Node<K,V> node = null, e; K k; V v;
            //如果 传入的参数与桶中头节点的hash值和key比对成功。
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;//用node记录已经找到的节点
            //如果 此桶中头节点的后面还有节点。
            else if ((e = p.next) != null) {
                //如果 头节点的后面是树结构
                if (p instanceof TreeNode)
                    //则 在树中返回查询结果
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                //否则表示,头节点后面是链表结构
                else {
                    //利用循环向后遍历链表中的节点,挨个比对它们的hash值和key
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            //至此,已经用node记录了最终的查找结果
            //若 node != null,则表示找到了此key对应的节点
            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;//将当前node节点从链表中移除
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

4.查找get(Object key)方法

	//查找:根据 键key,返回 值value
    public V get(Object key) {
        Node<K,V> e;
        //getNode()方法:根据 key的hash值 和 键key,返回对应Node节点
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

   
    //根据 key的hash值 和 键key,返回对应Node节点
    final Node<K,V> getNode(int hash, Object key) {
        //tab:保存当前数组的地址引用
        //first:某个桶中的头节点
        //n :记录table数组的长度
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        //如果 数组表table此hash值对应的桶中有节点
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            //如果 传入的参数与数组对应桶中,头节点的hash值相等,key也相同
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                //表示此hash桶中的头节点,就是我们要找的节点
                return first;
            //如果 此hash值对应桶中的节点不只是这一个头节点
            if ((e = first.next) != null) {
                //如果 头节点后面连的是个树
                if (first instanceof TreeNode)
                    //在树中返回节点
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);

                //否则表示,此hash值对应桶中的节点后面是个链表结构
                //则依次向后遍历每个节点,比较其hash值和key
                do {
                    //如果hash值和key比对成功
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;//则返回此节点
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

5.修改replace()

1>.replace(K key, V value)方法:

 	//修改:把key的值更新为value
    @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;//修改value
            afterNodeAccess(e);
            return oldValue;
        }
        return null;
    }

2>.replace(K key, V oldValue, V newValue)方法:

	//修改:当传入的key和oldValue都比对成功后,才将值更新为newValue
    @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;
    }

四、核心方法

1.hash(Object key)方法:

 	/**
     * 作用:为了让 key.hashCode()的高16位 ,也参与 异或运算(相同为0,不同为1)。
     *
     * 例:       h = 0010 0101 1010 1100 0011 1111 0010 1110
     * 则:h >>> 16 = 0000 0000 0000 0000 0010 0101 1010 1100
     *
     *         0010 0101 1010 1100 0011 1111 0010 1110
     *      ^  0000 0000 0000 0000 0010 0101 1010 1100
     *      =  0010 0101 1010 1100 0001 1010 1000 0010
     * 返回结果:高16位不变,低16位和高16位做了异或运算。
     *
     * 当哈希表的长度较小时,比如最开始默认的16,即表示可用索引index的范围是[0,15],此时非常容易产生冲突。
     * 为了在计算索引值index时,降低冲突的可能性,
     * 索引的计算方式为:index = (table.length - 1) & (高16位^低16位后得到的 hash值),
     *
     * 当多个key.hashCode()值的高16位不同,而低16位完全相同时,
     * 通过这样的方式计算索引值,由于影响索引值的因素包括高16位,得到的索引值index也就不易重复了,
     * 从而可以在一定程度上减少地址冲突的可能性。
     */
    //经过计算,返回 key的hash值
    static final int hash(Object key) {
        int h;
        //如果key!=null,借助hashCode()方法,计算key的hash值。
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

2.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.
     *
     * 扩容方法:
     * 初始化或加倍表的大小。
     * 如果为空,则按照字段阈值中默认的初始容量目标进行分配。
     * 否则,将使用2的整数次幂进行扩容,
     * 每个容器中的元素必须保持相同的索引,或者在新表中以2的幂移动。
     *
     * @return the table
     */
    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;//oldTab:用于保存扩容前旧表的地址引用
        //oldCap:用于记录扩容前table数组的长度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;//扩容之前的扩容阈值
        //newCap:扩容之后的table数组的长度
        //newThr:扩容之后,下次再触发的扩容阈值
        int newCap, newThr = 0;
        //如果旧表被初始化过,表示这是一次正常的扩容
        if (oldCap > 0) {
            //如果旧表数组长度已经超过最大容量
            if (oldCap >= MAXIMUM_CAPACITY) {
                //则不再继续扩充,且设置下次扩容条件为 threshold = Integer.MAX_VALUE.
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //如果旧表数组长度没超过最大容量
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                //则新数组长度扩容为旧数组长度的2倍.
                newThr = oldThr << 1; // double threshold
        }

        //此时 oldCap == 0,表示hashMap中的旧数组还没有被初始化过.
        //如果 旧的扩容阈值oldThr > 0.
        //注意:3个有参构造方法会给扩容阈值threshold赋值,无参构造方法不会
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;

        //此时 oldCap == 0,表示hashMap中的旧数组还没有被初始化过.
        //如果 旧的扩容阈值oldThr == 0.
        //则表示之前的hashMap是通过无参构造函数创建的,扩容阈值没有被赋过值.
        else {               // zero initial threshold signifies using defaults
            //给 新数组长度newCap 赋默认值16
            newCap = DEFAULT_INITIAL_CAPACITY;
            //给 新的扩容阈值newThr 赋默认值0.75*16 = 12
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }

        //如果 新扩容阈值newThr == 0
        if (newThr == 0) {
            //就计算一个 新扩容阈值newThr
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }

        threshold = newThr;//把 新的扩容阈值 赋给 全局变量
        //至此,计算出了 扩容后新数组的长度newCap 和 下次触发扩容函数的新阈值newThr
        //接下来,开始进行扩容操作.

       @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//创建了一个长度更长的新数组.
        table = newTab;//将扩容后的数组newTab的地址引用返回给table

        //如果 oldTab != null,则表示旧数组被初始化过(即旧表中有数据).
        if (oldTab != null) {

            //通过循环挨个遍历旧桶,并把旧桶内的元素移动到新桶中.
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                //如果旧数组中的某个桶内有数据(单个节点,链表或红黑树都有可能)
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;//资源回收

                    //如果当前桶内只有一个节点,无后继节点.
                    if (e.next == null)
                        //根据新数组长度重新计算索引,然后将此节点元素e存入到新表newTab中.
                        newTab[e.hash & (newCap - 1)] = e;

                    //如果当前桶内的节点是个树节点,则表示后面是个红黑树.
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    //如果后面是个链表结构
                    else { // preserve order
                        //低位链表:存放数组扩容前后,数组索引值未发生变化的节点.
                        //索引值不发生变化:新索引 = 旧索引。
                        //loHead作为低位链表的表头,loTail用来做链表的动态拼接.
                        Node<K,V> loHead = null, loTail = null;
                        //高位链表:存放数组扩容前后,数组索引值会发生变化的节点.
                        //索引值发生变化:新索引 = 旧索引 + 旧表长度。
                        //hiHead作为高位链表的表头,hiTail用来做链表的动态拼接.
                        Node<K,V> hiHead = null, hiTail = null;

                        Node<K,V> next;
                        do {
                            next = e.next;
                            //若 e.hash = 15 = 0 1111 ,oldCap = 16 = 1 0000
                            //则 e.hash & oldCap = 0 1111 & 1 0000  = 0 0000 = 0
                            //表示 扩容前后,索引值不变:新索引 = 旧索引.
                            //此节点是个低位链表中的节点.
                            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);

                        //如果低位链表中有数据
                        if (loTail != null) {
                            loTail.next = null;//用于标记尾节点,给尾节点的后继节点赋成null.
                            //由于扩容前后,低位链表内节点的索引值不需要发生改变.
                            //所以直接通过低位链表的表头,把整个链表添加到新数组的对应桶中.
                            newTab[j] = loHead;
                        }
                        //如果高位链表中有数据
                        if (hiTail != null) {
                            hiTail.next = null;//用于标记尾节点,给尾节点的后继节点赋成null.
                            //由于扩容前后,高位链表内节点的索引值会发生改变.
                            //所以通过高位链表的表头,把整个链表添加到新数组的对应桶中.
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

3.扩容机制

在这里插入图片描述
解析:

当桶内节点个数不止一个,且后面的结构是链表时,其扩容图示如上:

以索引值为15的桶为例,在图示中有5个节点元素。

我们已知,索引的计算方式 index = (key.hash) &(table.length - 1).

当扩容前的数组长度为16时,table.length - 1 = 16 - 1 = 15 = 1111 B;

由索引值index = 15 = 1111 B = (key.hash) &(table.length - 1) = (key.hash) & 1111 B,

得知,key的hash值的后4位 key.hash = 1111 B。

当数组长度扩容为32时,table.length - 1 = 32 - 1 = 31 = 0001 1111 B;

在旧表中,索引 index = 15 位置处的节点,由上述可知,其key的hash值的后四位是 1111 B。

对应到长度为32的新表中计算其索引值,index = (key.hash) &(table.length - 1)。

此时,由于key.hash高位数值的不同,最终结果存在两种可能。

1.若 key.hash = xxx0 1111 B,

则index = (key.hash) &(table.length - 1) = xxx0 1111 & 0001 1111 =0000 1111 = 15。

所以,存在原数组索引值为15桶中的元素,存到了新数组中索引值为15的桶中(索引值没变)。

2.若 key.hash = xxx1 1111 B,

则index = (key.hash) &(table.length - 1) = xxx1 1111 & 0001 1111 =0001 1111 = 31。

所以,存在原数组索引值为15桶中的元素,存到了新数组中索引值为31的桶中(索引值发生变化)。

综上所述,扩容前后的索引值存在两种对应关系。

一种是,索引值不发生变化:新索引 = 旧索引。

另一种是,索引值发生变化:新索引 = 旧索引 + 旧表长度。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值