HashMap(Java8)原理解析&&HashMap扩容机制

一、知识储备

(一) HashMap 继承体系

仅提供我们需要关心的几个map关系
Map继承体系

(二) HashMap 数据结构

    /**
     * Basic hash bin node, used for most entries.  (See below for
     * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
     */
    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;
        }

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

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

HashMap存储数据都是由Node负责的,每一个Node都存储了一对键值对key-value;
并且它关联了下一个next节点,从这里就能看到单链表的影子了。

在这里插入图片描述
HashMap随着容量(size)和hash碰撞的次数不断增长,HashMap的结构也在处于不同状态:

  • 当它的存入的节点Node未发生hash碰撞时候,HashMap只是一个普通的数组;
  • 发生hash碰撞,所在节点发生**链化**,节点Node升级为链表,hash值相同的节点相继存入这个链表中;
  • 当散列表(table)size > 64,链的长度超过8,该链发生树化,链表转化为红黑树

(三) Hash原理,Hash碰撞,以及链表中的hash值

1. Hash原理

Hash散列函数-百度百科)也称散列哈希,对应的英文都是Hash。 基本原理就是把任意长度的输入,通过Hash算法变成固定长度的输出。 这个映射的规则就是对应的Hash算法,而原始数据映射后的二进制串就是哈希值。 … 整个Hash算法的过程就是把原始任意长度的值空间,映射成固定长度的值空间的过程。

Java中的hashcode怎么来的,可参考——知乎

Hash的特点:

  • 从hash值不可以反向推导出原始的数据;
  • 输入数据的微小变化会得到完全不同的hash值,相同的数据会得到相同的值;
  • 哈希算法的执行效率要高效,长的文本也能够很快计算出哈希值;
  • hash算法的冲突概率要小

2. Hash碰撞(哈希碰撞)

由于hash算法的原理是将输入空间的值映射称hash空间内的值,而hash值的空间的远远小于输入的空间。根据抽屉原理,一定会存在不同的输入被映射成相同的输出的情况,即哈希碰撞。

▲抽屉原理

抽屉原理的一般含义为:“如果每个抽屉代表一个集合,每一个苹果就可以代表一个元素,假如有n+1个元素放到n个集合中去,其中必定有一个集合里至少有两个元素。” 抽屉原理有时也被称为鸽巢原理。 它是组合数学中一个重要的原理 。

因为存在哈希碰撞,所以不能用hash值来区分对象的唯一性,但是却可以区分不同性,即:相同的对象哈希值一定相同,哈希值不同,两个对象一定不同

3.链表中的hash值

需要注意,链表中node的hash值并不是hashcode,而是hashcode经过哈希扰动之后的值,后面会讲到。

(四)链化、树化

1.链化

什么是链化?为什么要链化?
当发生hash碰撞的时候,就在数组对应的位置生成一条链表。链化是为了解决hash碰撞。

哈希碰撞其他解决办法

  • 开放地址法;
  • 再哈希法;
  • 链地址法;
  • 建立公共溢出区

2.树化

当链表长度达到阈值 TREEIFY_THRESHOLD = 8,散列表length达到最小树化容量MIN_TREEIFY_CAPACITY = 64,该链表即升级为红黑树TreeNode。

3. 为什么要树化使用红黑树?为什么不一开始使用红黑树?

因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡,而单链表不需要。
当链表元素小于8的时候,此时做查询操作,链表结构已经能保证查询性能。当元素大于8个的时候,节点变多的复杂度就变成O(n)了,此时需要红黑树来加快查询速度;
红黑树新增节点的效率变慢了。因此,如果一开始就用红黑树结构,元素太少,新增效率又比较慢,无疑这是浪费性能的。


二、 核心原理

(一)HashMap核心变量

  /**
     * The default initial capacity - MUST be a power of two.
     * HashMap默认容量(数组长度),这个数指必须时2的n次幂
     */
    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.
     * table的最大容量(长度)
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * The load factor used when none specified in constructor.
     * 缺省负载因子大小。
     * 这个值时经过大量数据实验得到的最佳值
     */
    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.
     * 树化阈值。
     * 当发生hash碰撞后,会链化。当链表长度达到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.
     * 树降级成为链表的阈值
     */
    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.
     * 当hash表length达到这个值,并且某个链的长度达到8,才允许树化
     */
    static final int MIN_TREEIFY_CAPACITY = 64;

  /**
     * 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).
     * hash表结构修改次数
     */
    transient int modCount;

    /**
     * The next size value at which to resize (capacity * load factor).
     *
     * @serial
     * 扩容阈值,当hash表中的元素超过阈值时,触发扩容
     * threshold = capacity * loadfactor
     */
    // (The javadoc description is true upon serialization.
    // Additionally, if the table array has not been allocated, this
    // field holds the initial array capacity, or zero signifying
    // DEFAULT_INITIAL_CAPACITY.)
    int threshold;

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

这几个变量尤为重要,虽然我在上面写的已经够清楚了,但是还是需要再详细说一下:

DEFAULT_INITIAL_CAPACITY

Hash表初始化默认初始容量,即table的length;注意散列表的容量capacity一定是2的n次幂!

什么时候扩容?

loadFactor:负载因子。
DEFAULT_LOAD_FACTOR :默认的负载因子
threshold:触发扩容的阈值。threshold = capacity * loadFactor 。当散列表实际使用容量达到扩容阈值,触发扩容。

什么时候树化?

TREEIFY_THRESHOLD :树化阈值,值为8;
MIN_TREEIFY_CAPACITY:最小树化容量阈值,值为64。
当实际使用容量达到64,且某一条链长度达到8,该链发生树化,单链表转化为红黑树。

红黑树什么时候降级为链表?为什么

UNTREEIFY_THRESHOLD红黑树降级为链表的阈值,值为6.

6和8之间存在一个差值7可以防止HashMap频繁插入、删除元素,链表元素个数在8左右变化,从而引起链表和红黑树的频繁转换,降低hashmap的效率

(二)HashMap的构造函数

   /**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and load factor.
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    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);
    }

    /**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and the default load factor (0.75).
     *
     * @param  initialCapacity the initial capacity.
     * @throws IllegalArgumentException if the initial capacity is negative.
     */
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    /**
     * Constructs an empty <tt>HashMap</tt> with the default initial capacity
     * (16) and the default load factor (0.75).
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

    /**
     * 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>.
     *
     * @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);
    }



(三)默认容量计算

 /**
     * Returns a power of two size for the given target capacity.
     */
    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;
    }

容量不用我们自己操心,这个算法返回一个大于等于传入的容量值的 —— 2的n次幂的数作为容量。

(四)put方法

 	/**
     * 在表中将键值关联起来。
     * 如果表中已经存在key对应的键值对,就替换其旧的值
     */
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }


   /**
     * Computes key.hashCode() and spreads (XORs) higher bits of hash
     * to lower.  Because the table uses power-of-two masking, sets of
     * hashes that vary only in bits above the current mask will
     * always collide. (Among known examples are sets of Float keys
     * holding consecutive whole numbers in small tables.)  So we
     * apply a transform that spreads the impact of higher bits
     * downward. There is a tradeoff between speed, utility, and
     * quality of bit-spreading. Because many common sets of hashes
     * are already reasonably distributed (so don't benefit from
     * spreading), and because we use trees to handle large sets of
     * collisions in bins, we just XOR some shifted bits in the
     * cheapest possible way to reduce systematic lossage, as well as
     * to incorporate impact of the highest bits that would otherwise
     * never be used in index calculations because of table bounds.
     * hash扰动
     * 让key的hash值的高16位参与路由运算
     */
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

当key位null的时候,直接将这个键值对存放到map的第一个。通过计算得到数组中将要存放元素的下标
补充一个知识点:

  • 异或^ ,位运算相同为 0,不同为 1
    Eg: h = 0010 0101 1010 1001 0010 0110 1100 1011
    h无符号右移16位,得到其高16位的数:
    0000 0000 0000 0000 0010 0101 1010 1001
    ^h:
    0010 0101 1010 1001 0010 0110 1100 1011
    =>
    0010 0101 1010 1001 0000 0010 0100 0010

真正存放元素的方法


  final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        //tab当前HashMap的散列表
        //p:散列表中当前路由寻址下标元素
        //n:当前散列表数组的length
        //i:散列表中当前路由寻址的结果
        Node<K,V>[] tab; Node<K,V> p; int n, i;

        //将散列表数组赋值给tab
        //tab==null或者数组是空的,说明散列表没有初始化,初始化数组
        //作用,延迟初始化逻辑,因为创建HashMap第一时间不一定存放数据,防止浪费内存
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //- scenario 1 -最简单的一种情况:通过路由寻址,计算出所在下标。取出下标对应Node,如果为null,创建一个新的node直接存放
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
        	//如果对应下标不为null,hash碰撞;
      
        	//e:找到的已存在的与当前要插入的key-value的key一致的元素Node。
        	//注意这个元素不一定存在!如果hash表中添加一个新的键值对的时候,它就不存在
        	//k:临时的一个key
            Node<K,V> e; K k;
            //比对已存在的node的key值与其hash和当前存放的key的值和其hash值时否一致
        	//-scenario 2- key的hash值和key值完全一致,表示已存在元素和插入键值对key-value的key一致,
        	//则临时取出已存在的node,后续准备替换其值value,直接执行 scenario 4 
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
                //-scenario 3- 通过比对发现不是同一个key
                //如果是红黑树,表示已经树化了,则在树中存放
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

                //-scenario 4- 不是红黑树,不满足 链表长度=8 && hash表size=64,则是链表
            else {
            //循环当前链表
                for (int binCount = 0; ; ++binCount) {
                    //-scenario 4-1-循环到末尾未找到key相等的元素,说明链表中没有已存在key值相同的键值对node,
                    //就在其末尾插入新的node,跳出循环
                    //注意这里e=null
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //如果操作数大于等于 树化阈值,触发树化
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        	//树化
                            treeifyBin(tab, hash);
                        break;
                    }
                    //-scenario 4-2-找到key值和hash完全一致的node,即链表中已存在目标node,找到这个node,
                    //后续准备value替换,跳出循环进行 scenario 5
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //-scenario 5- 替换已经存在的键值对node的value,并返回旧的值
            // 只有scenario 1 和scenario 3-2 会发现已存在的元素,然后执行这里。
            //注意这里是替换操作,操作数不会变化
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        //三类表结构修改次数+1,替换不计数,只有增加元素的时候才会计数
        ++modCount;
        //增加元素之后,如果size>扩容阈值,触发扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }


 /**
     * Replaces all linked nodes in bin at index for given hash unless
     * table is too small, in which case resizes instead.
     */
    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        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);
        }
    }

    

put方法分4种情况:
首先检查发现Node[] table 为null,说明HashMap虽然实例化,但是散列表并没有初始化,此时调扩容方法resize初始化;

  • scenario 1:通过路由寻址算法找到下标(其它作者称为桶位),判断该位置元素是否存在,如果不存在,直接new一个存放;
    如果不为空,这又需要看情况:
  • scenario 2:如果【该位置的元素的hash值和正要存入的键值对的key的hash值一致并且key经过equals比较一致,或,key为同一个对象】,这说明该位置第一个(不管它是链表还是红黑树)就是我们要替换的元素对象,把这个节点元素暂存到 变量 e ,待后续修改value值;
  • scenario 3: 如果该节点是红黑树,则调用TreeNode.putTreeVal方法,这个方法也是检查到红黑树中已存在包含这个key的节点就返回也是暂存到变量 e,没有就创建一个并存入:这个方法属于红黑树的范畴了,写这篇帖子的时候,我还不懂红黑树,所以我不具体展开讲它的原理,可能存在误区)。
  • scenario 4:当前节点是链表。开始迭代链表
  • scenario 4-1 : 简单情况。【链表中存在某个元素的hash值和正要存入的键值对的key的hash值一致且key值一致,或者,key为同一个对象】则表示存在key一致的键值对元素,那么停止循环,元素已经暂存到e上,留到后面替换value;

注意我画了两次的重点:链表中存在某个元素的hash值和正要存入的键值对的key的hash值一致且key值一致,或者,key经过equals比较一致。则表示存在key一致的键值对元素。这就引出了一个问题:

△什么对象可以作为HashMap的key?,或者说,△成为HashMap的key需要满足什么条件?

实体类重写hashCode方法和equals方法,使得对象的值相同时,产生的hash值一致;最好是变量私有化,不保留修改它元素的方法甚至setter,仅仅保留构造函数中的传值方式,以达到对象不变的目的。

△可不可以不重写equals? (可参考String)

不可以! 因为自定义实体类默认没有重写Object的equals方法,该方法是比较对象地址是否一致的。所以不重写严重后果是,命名hash值相同,key值一致对象的元素一致却无法取出hashMap中的元素,或者没有办法替换已存在键值对的值。

好,跳过这段插曲,继续讲:

  • scenario 4-2:复杂情况。迭代知道链表的尾部也没有可替换的键值对元素,那么就new一个存入并存入队尾。这不是重点,重点是后面:如果当前链表长度大于等于树化阈值(注意此处阈值-1是因为迭代时候下标是从0开始的),满足条件则调树化的方法treeifyBin:

链表升级为树 treeifyBin:

    /**
     * Replaces all linked nodes in bin at index for given hash unless
     * table is too small, in which case resizes instead.
     */
    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        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);
        }
    }

如果散列表容量小于最小树化容量则扩容,如果大于,则将当前链表升级为树(这里我不展开讲)。可以看到,链表树化需要达到两个条件某个链表长度达到树化阈值,散列表容量达到最小树化阈值容量

(五)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.
     *
     * @return the table
     */
    final Node<K,V>[] resize() {
    	//应用扩容之前的hash表
        Node<K,V>[] oldTab = table;
        //扩容之前的数组长度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //扩容之前的扩容阈值(触发本次扩容的阈值)
        int oldThr = threshold;
        //扩容之后数组长度,// 扩容之后再次触发扩容阈值
        int newCap, newThr = 0;
        //已经初始化过了,是一次正常扩容
        if (oldCap > 0) {
        	//如果之前的数组长度已经达到最大值了,则不扩容,设置扩容阈值位int最大值。这种情况很少
            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
        }
        //oldCap==0,调用构造器传入了容量,然后调用tableForSize计算出了thresshold
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
            //调了无参的构造方法。初始化 容量和阈值为默认值
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        //如果调用构造器传入了容量,那么这个值就是0,然后一般情况是通过负载因子和容量计算得到值
        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;
        //如果扩容之前hash表中已经不为null了
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                //当前位置由数据
                if ((e = oldTab[j]) != null) {
                	//就数组这个位置置空
                    oldTab[j] = null;
                    //如果这个节点是单个数据,从未发生hash碰撞,非链,非红黑树。
                    //通过录用寻址得到的新数组下标后,直接存进去
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                        //如果是红黑树,已经树化
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                        //当前节点是链表
                    else { // preserve order
                    	// lowHead:低位的链表头部,lowTail,低位的链表尾部
                        Node<K,V> loHead = null, loTail = null;
                        // highHead:高位的链表首部,highTail:高位的链表尾部
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            //通过hash值和之前的数组长度(没有-1)与运算,得到高位,要么是1,要么是0
                            //1-则存放到新的数组中的后半段,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) {
                        	//置为null,因为它的下一个很可能就是高位的链表中某一个
                            loTail.next = null;
                            //仍然存放到低位的同样位置
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                        	//置为null,因为他们在之前的链表中,关联了下一个节点,很可能在低位的链表中
                            hiTail.next = null;
                            //存放到高位的(原来位置+原来数组长度)的位置
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

扩容方法实在太长了,上面注释也比较清楚,我就不写解析了 😂😂😂

为什么要扩容?

元素少的时候,未发生hash碰撞之前,hash表是数组结构,存放元素时间复杂度是O(1),当hash表树化以后,时间复杂度变成O(n),即每次先找到数组中对应路由地址,然后迭代链表,查找效率退化,性能变低;通过扩容缓解了 哈希冲突导致的莲花影响查询效率的问题。

(六)get方法


    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) {
    	//tab:当前hash表的散列表table
    	//first:当前位置的头元素
    	//e:临时节点
    	//n:当前散列表table长度
    	//k:临时节点的k
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        //散列表不为空的情况下,通过路由寻址算法(hash &(length-1))定位到第一个元素的下标并取出
        //此时这个元素可能是数组的普通元素,也可能是链,也可能是树
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            // scenario 1 ,最简单情况,第一个元素 就是我们要的node
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
                //并非单个元素,可能链化或者树化了
            if ((e = first.next) != null) {
            	// scenario 2 ,当前元素已经树化
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                    // scenario 3 ,当前元素链化
                do {
                //不断循环直到取出目标node
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

(七)remove

 /**
     * Removes the mapping for the specified key from this map if present.
     *
     * @param  key key whose mapping is to be removed from the map
     * @return the previous value associated with <tt>key</tt>, or
     *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
     *         (A <tt>null</tt> return can also indicate that the map
     *         previously associated <tt>null</tt> with <tt>key</tt>.)
     */
    public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }

    /**
     * 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) {
        //tab:引用当前Hash表
        //p:当前node元素
        //n:当前散列表长度
        //index:寻址结果             
        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:目标node
            //e:循环中的当前元素
            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 {
                //如果已经链化
                //循环取出目标节点node和它上一个节点p
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            //如果目标节点不为null,并且结果匹配(value不必要相同 或 必要相同时value一致)
            //进行后面的remove操作
            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);
                 //目标节点和之前记录的上个节点时时同一个,即在table中该位置头元素就是删除的目标
                else if (node == p)
                    tab[index] = node.next;
                    //链化,把当前节点的next节点关联到上一个节点,抛弃当前节点
                else
                    p.next = node.next;
                //操作次数加1
                ++modCount;
                //size -1 
                --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;
    }

没什么好说的,调用了getNode找到目标node,然后替换其值


三、 补充

这里面涉及 位运算,我并不熟悉,看了半天别人的帖子五花八门我并不能真的理解,所以没有解析;

另外提到树化,树的降级等红黑树相关,我并没有在这里展开讲,后续我会更新红黑树相关的帖子;

借鉴文章:

HashMap面试必问的6个点,你知道几个?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值