Java集合之HashMap源码分析(面试总结)

梳理HahsMap知识,文章结构及思路如下: 

目录

一、主要特点

二、继承关系

三、数据结构

数组+链表+红黑树(JDK1.8增加了红黑树部分)

主要元素 

四、核心方法解析

hash() 

comparableClassFor()

tableSizeFor()

HashMap() 

get()

put()

resize()

treeifyBin()

compute() 

五、面试题

hashMap与hashTable区别

谈一下1.7和1.8中hashMap的优化

HashMap是线程安全的吗?有什么办法线程安全吗

hash函数是怎么设计的

能详细说一下嘛,这样做有什么好处

为什么这样设计可以增加散列性

LinkedHashMap怎么实现有序的

讲一下几个主要函数的逻辑思路吧 get(),put(),resize(),replace(),remove()

六、参考资料


一、主要特点

  • 底层实现是 链表数组+红黑树,拉链法
  • key 用 Set 存放,不允许重复,key 如果用对象则需要重写 hashCode 和 equals 方法
  • 允许空键和空值,但空键只有一个
  • 元素是无序的,而且顺序会不定时改变
  • 插入、获取的时间复杂度基本是 O(1)(前提是有适当的哈希函数,让元素分布在均匀的位置)
  • 两个关键因子:初始容量、加载因子

二、继承关系

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

三、数据结构

数组+链表+红黑树(JDK1.8增加了红黑树部分)

preview

主要元素 

    /**
     * 默认初始容量16——必须是2的幂
     * 01向左补四位,2的四次方
     * hashCode & (length-1); 15位与14位相比,与hashcode相与会有更多的结果,且不浪费空间
     * 所以将length定位二次幂,在进行hash运算时,不同的key算得index相同的几率较小,那么数据在数组上分布就比较均匀,
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    /**
     * 最大容量,必须是2的幂 2的30次方
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * 载荷因子
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * treeify_threshold由链表转化为红黑书的阀值
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * 红黑树节点转换链表节点的阈值
     */
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     * 转红黑树时数组应该满足的长度
     * 至少是 4 * TREEIFY_THRESHOLD ,节省效率
     */
    static final int MIN_TREEIFY_CAPACITY = 64;

    /**
     * 基本的哈希节点,链表节点, 继承自Entry
     * k,v是Map<k,v>传入的数据类型
     */
    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;
        }
        @Override
        public final K getKey()        { return key; }
        @Override
        public final V getValue()      { return value; }
        @Override
        public final String toString() { return key + "=" + value; }
        @Override
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }
        @Override
        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }
        @Override
        public final boolean equals(Object o) {
            //存储位置相同
            if (o == this) {
                return true;
            }
            //instanceof是Java中的一个双目运算符,用来测试一个对象是否为一个类的实例
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                return Objects.equals(key, e.getKey()) && Objects.equals(value, 
    e.getValue());
            }
            return false;
        }
    }
    //将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会被序列化。
    //table数组
    transient Node<K,V>[] table;

    /**
     * Holds cached entrySet(). Note that AbstractMap fields are used
     * for keySet() and values().
     */
    transient Set<Map.Entry<K,V>> entrySet;

    // 大小
    transient int size;

    transient int modCount;

    /**
     * 转化为红黑树的阀值
     */
    int threshold;
    /**
     * 哈希表的负载系数。
     */
    final float loadFactor;

 好的Hash算法和扩容机制,可以使Hash碰撞的概率又小,哈希桶数组(Node[] table)占用空间又少

四、核心方法解析

hash() 

第一步,拿到key.hashCode()

第二步高16位异或运算
(>>> 表示无符号右移,也叫逻辑右移,即若该数为正,则高位补0,而若该数为负数,则右移后高位同样补0)

Hash算法本质上就是三步:取key的hashCode值、高位运算、取模运算

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

这里借用一个图来表示整个key.hashcode()变化到下标的处理过程

 

 通过这个计算过程可以看出,生成的数组下标会因为‘扰动’的增加而减少碰撞的机率。

comparableClassFor()

 /**
     * Returns x's Class if it is of the form "class C implements
     * Comparable<C>", else null.
     */
    static Class<?> comparableClassFor(Object x) {
        if (x instanceof Comparable) {
            Class<?> c; Type[] ts, as; Type t; ParameterizedType p;
            if ((c = x.getClass()) == String.class) // bypass checks
                return c;
            if ((ts = c.getGenericInterfaces()) != null) {
                for (int i = 0; i < ts.length; ++i) {
                    if (((t = ts[i]) instanceof ParameterizedType) &&
                            ((p = (ParameterizedType)t).getRawType() ==
                                    Comparable.class) &&
                            (as = p.getActualTypeArguments()) != null &&
                            as.length == 1 && as[0] == c) // type arg is c
                        return c;
                }
            }
        }
        return null;
    }

    /**
     * Returns k.compareTo(x) if x matches kc (k's screened comparable
     * class), else 0.
     */
    @SuppressWarnings({"rawtypes","unchecked"}) // for cast to Comparable
    static int compareComparables(Class<?> kc, Object k, Object x) {
        return (x == null || x.getClass() != kc ? 0 :
                ((Comparable)k).compareTo(x));
    }

   

tableSizeFor()

 【作用】返回给定目标容量的2倍幂。将我们传入的容量设置为大于并最接近的2^N

 【解读】

详见:求一个数字大于并最接近的2^N

   //补位,将原本为0的空位填补为1,最后加1时,最高有效位进1,其余变为0,如此就可以取到最近的2的幂
    static final int tableSizeFor(int cap) {
        //减一后,最右一位肯定和cap的最右一位不同,即一个为0,一个为1
        int n = cap - 1;
        //(>>>)无符号右移一位,(|)按位或
        n |= n >>> 1;
        //(>>>)无符号右移两位,(|)按位或
        n |= n >>> 2;
        //(>>>)无符号右移四位,(|)按位或
        n |= n >>> 4;
        //(>>>)无符号右移八位,(|)按位或
        n |= n >>> 8;
        //(>>>)无符号右移十六位,(|)按位或,为何到16呢,存疑
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

HashMap() 

  指定了初始容量和加载因子,会对参数进行校验
  初始容量不能为负数,不能大于最大容量 1 << 30 (2^30)


    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0) {
            throw new IllegalArgumentException("Illegal initial capacity: " + 
    initialCapacity);
        }
        if (initialCapacity > MAXIMUM_CAPACITY) {
            initialCapacity = MAXIMUM_CAPACITY;
        }
        if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
            throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
        }
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }

    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
    }

    /**
     *
     * @param   m the map whose mappings are to be placed in this map
     * @throws  NullPointerException if the specified map is null
     */
    public HashMap1(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

get()

public V get(Object key) {
    Node<K,V> e;
    //还是先计算 哈希值
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    //tab 指向哈希表,n 为哈希表的长度,first 为 (n - 1) & hash 位置处的桶中的头一个节点
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        //如果桶里第一个元素就相等,直接返回
        if (first.hash == hash &&
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        //否则就得慢慢遍历找
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                //如果是树形节点,就调用树形节点的 get 方法
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                //do-while 遍历链表的所有节点
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

put()

这里看到知乎一个大神的相关回答,图片描述非常形象,引用如下:

(大神回答链接如下:https://zhuanlan.zhihu.com/p/21673805

preview

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

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
 
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 1.校验table是否为空或者length等于0,如果是则调用resize方法进行初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 2.通过hash值计算索引位置,将该索引位置的头节点赋值给p,如果p为空则直接在该索引位置新增一个节点即可
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        // table表该索引位置不为空,则进行查找
        Node<K,V> e; K k;
        // 3.判断p节点的key和hash值是否跟传入的相等,如果相等, 则p节点即为要查找的目标节点,将p节点赋值给e节点
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 4.判断p节点是否为TreeNode, 如果是则调用红黑树的putTreeVal方法查找目标节点
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            // 5.走到这代表p节点为普通链表节点,则调用普通的链表方法进行查找,使用binCount统计链表的节点数
            for (int binCount = 0; ; ++binCount) {
                // 6.如果p的next节点为空时,则代表找不到目标节点,则新增一个节点并插入链表尾部
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 7.校验节点数是否超过8个,如果超过则调用treeifyBin方法将链表节点转为红黑树节点,
                    // 减一是因为循环是从p节点的下一个节点开始的
                    if (binCount >= TREEIFY_THRESHOLD - 1)
                        treeifyBin(tab, hash);
                    break;
                }
                // 8.如果e节点存在hash值和key值都与传入的相同,则e节点即为目标节点,跳出循环
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;  // 将p指向下一个节点
            }
        }
        // 9.如果e节点不为空,则代表目标节点存在,使用传入的value覆盖该节点的value,并返回oldValue
        if (e != null) {
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e); // 用于LinkedHashMap
            return oldValue;
        }
    }
    ++modCount;
    // 10.如果插入节点后节点数超过阈值,则调用resize方法进行扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);  // 用于LinkedHashMap
    return null;
}

resize()

扩容,1.7的扩容里,变化长度后所有的hashcode都要重新计算,消耗较大。1.8的扩容里,机制变得更加巧妙,节省计算,具体变化以后再进行学习。

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    // 1.老表的容量不为0,即老表不为空
    if (oldCap > 0) {
        // 1.1 判断老表的容量是否超过最大容量值:如果超过则将阈值设置为Integer.MAX_VALUE,并直接返回老表,
        // 此时oldCap * 2比Integer.MAX_VALUE大,因此无法进行重新分布,只是单纯的将阈值扩容到最大
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 1.2 将newCap赋值为oldCap的2倍,如果newCap<最大容量并且oldCap>=16, 则将新阈值设置为原来的两倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    // 2.如果老表的容量为0, 老表的阈值大于0, 是因为初始容量被放入阈值,则将新表的容量设置为老表的阈值
    else if (oldThr > 0)
        newCap = oldThr;
    else {
        // 3.老表的容量为0, 老表的阈值为0,这种情况是没有传初始容量的new方法创建的空表,将阈值和容量设置为默认值
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 4.如果新表的阈值为空, 则通过新的容量*负载因子获得阈值
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    // 5.将当前阈值设置为刚计算出来的新的阈值,定义新表,容量为刚计算出来的新容量,将table设置为新定义的表。
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    // 6.如果老表不为空,则需遍历所有节点,将节点赋值给新表
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {  // 将索引值为j的老表头节点赋值给e
                oldTab[j] = null; // 将老表的节点设置为空, 以便垃圾收集器回收空间
                // 7.如果e.next为空, 则代表老表的该位置只有1个节点,计算新表的索引位置, 直接将该节点放在该位置
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                // 8.如果是红黑树节点,则进行红黑树的重hash分布(跟链表的hash分布基本相同)
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    // 9.如果是普通的链表节点,则进行普通的重hash分布
                    Node<K,V> loHead = null, loTail = null; // 存储索引位置为:“原索引位置”的节点
                    Node<K,V> hiHead = null, hiTail = null; // 存储索引位置为:“原索引位置+oldCap”的节点
                    Node<K,V> next;
                    do {
                        next = e.next;
                        // 9.1 如果e的hash值与老表的容量进行与运算为0,则扩容后的索引位置跟老表的索引位置一样
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null) // 如果loTail为空, 代表该节点为第一个节点
                                loHead = e; // 则将loHead赋值为第一个节点
                            else
                                loTail.next = e;    // 否则将节点添加在loTail后面
                            loTail = e; // 并将loTail赋值为新增的节点
                        }
                        // 9.2 如果e的hash值与老表的容量进行与运算为非0,则扩容后的索引位置为:老表的索引位置+oldCap
                        else {
                            if (hiTail == null) // 如果hiTail为空, 代表该节点为第一个节点
                                hiHead = e; // 则将hiHead赋值为第一个节点
                            else
                                hiTail.next = e;    // 否则将节点添加在hiTail后面
                            hiTail = e; // 并将hiTail赋值为新增的节点
                        }
                    } while ((e = next) != null);
                    // 10.如果loTail不为空(说明老表的数据有分布到新表上“原索引位置”的节点),则将最后一个节点
                    // 的next设为空,并将新表上索引位置为“原索引位置”的节点设置为对应的头节点
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    // 11.如果hiTail不为空(说明老表的数据有分布到新表上“原索引+oldCap位置”的节点),则将最后
                    // 一个节点的next设为空,并将新表上索引位置为“原索引+oldCap”的节点设置为对应的头节点
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    // 12.返回新表
    return newTab;
}

treeifyBin()

/**
 * 将链表节点转为红黑树节点
 */
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    // 1.如果table为空或者table的长度小于64, 调用resize方法进行扩容
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    // 2.根据hash值计算索引值,将该索引位置的节点赋值给e,从e开始遍历该索引位置的链表
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;
        do {
            // 3.将链表节点转红黑树节点
            TreeNode<K,V> p = replacementTreeNode(e, null);
            // 4.如果是第一次遍历,将头节点赋值给hd
            if (tl == null)	// tl为空代表为第一次循环
                hd = p;
            else {
                // 5.如果不是第一次遍历,则处理当前节点的prev属性和上一个节点的next属性
                p.prev = tl;    // 当前节点的prev属性设为上一个节点
                tl.next = p;    // 上一个节点的next属性设置为当前节点
            }
            // 6.将p节点赋值给tl,用于在下一次循环中作为上一个节点进行一些链表的关联操作(p.prev = tl 和 tl.next = p)
            tl = p;
        } while ((e = e.next) != null);
        // 7.将table该索引位置赋值为新转的TreeNode的头节点,如果该节点不为空,则以以头节点(hd)为根节点, 构建红黑树
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

compute() 

    @Override
    public V compute(K key,
                     BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
        if (remappingFunction == null)
            throw new NullPointerException();
        int hash = hash(key);
        Node<K,V>[] tab; Node<K,V> first; int n, i;
        int binCount = 0;
        TreeNode<K,V> t = null;
        Node<K,V> old = null;
        if (size > threshold || (tab = table) == null ||
            (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((first = tab[i = (n - 1) & hash]) != null) {
            if (first instanceof TreeNode)
                old = (t = (TreeNode<K,V>)first).getTreeNode(hash, key);
            else {
                Node<K,V> e = first; K k;
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k)))) {
                        old = e;
                        break;
                    }
                    ++binCount;
                } while ((e = e.next) != null);
            }
        }
        V oldValue = (old == null) ? null : old.value;
        V v = remappingFunction.apply(key, oldValue);
        if (old != null) {
            if (v != null) {
                old.value = v;
                afterNodeAccess(old);
            }
            else
                removeNode(hash, key, null, false, true);
        }
        else if (v != null) {
            if (t != null)
                t.putTreeVal(this, tab, hash, key, v);
            else {
                tab[i] = newNode(hash, key, v, first);
                if (binCount >= TREEIFY_THRESHOLD - 1)
                    treeifyBin(tab, hash);
            }
            ++modCount;
            ++size;
            afterNodeInsertion(true);
        }
        return v;
    }

五、面试题

hashMap与hashTable区别

  1. HashMap 允许 key 和 value 为 null,Hashtable 不允许。
  2. HashMap 的默认初始容量为 16,Hashtable 为 11。
  3. HashMap 的扩容为原来的 2 倍,Hashtable 的扩容为原来的 2 倍加 1。
  4. HashMap 是非线程安全的,Hashtable是线程安全的。
  5. HashMap 的 hash 值重新计算过,Hashtable 直接使用 hashCode。
  6. HashMap 去掉了 Hashtable 中的 contains 方法。
  7. HashMap 继承自 AbstractMap 类,Hashtable 继承自 Dictionary 类。

谈一下1.7和1.8中hashMap的优化

  1. 底层数据结构,增加了红黑树这一结构,默认变化阀值是数组长度>64,链表长度>8,查找操作需要遍历node数组节点下的链表,当桶的长度过长时,会降低查询效率,所以1.8中增加红黑树,这种结构在数据量大的时候增删改查操作更快,当链表长度降低<=6时,红黑树重新转化为链表,链表O(n),红黑树O(logn)
  2. 优化高位运算的hash算法,h^(h>>>16),扰动四次变为扰动一次,提高效率
  3. 添加时,头插变为尾插,头插法会使链表发生反转,多线程环境下头插会产生环,尾插则不会发生反转,更安全
  4. 优化扩容机制,resize()扩容机制,扩容的阀值=设定容量*载荷因子(16*0.75),达到阀值时,扩容成当前的2倍。这中间消耗性能最大的就是扩容后index值的重新计算,index=h&(length-1),1.8中进行了优化,扩容后,元素要么是在原位置,要么是在原位置再移动2次幂的位置,且链表顺序不变

HashMap是线程安全的吗?有什么办法线程安全吗

 

  1. 不是线程安全,1.7中会出现链表循环,插入重复,在源码中没有关于锁的操作。
  2. 实现线程安全,有三种方法,Tablemap,ConcerrentHashMap,以及Collections.synchronizedMap,tableMap对整个操作方法进行锁定,粒度过大,基本不存在合适的使用场景,Collections.synchronizedMap是Collections里面的内部类,把map传入内部定义的带锁的synchronizedMap对象,可以实现线程安全,ConcerrentHashMap使用分段锁(锁住当前节点),CAS+synchronized,降低锁粒度,提高并发量

hash函数是怎么设计的

        先拿到key的hashcode(32位)然后让hashcode的高16与低16位进行异或运算

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

能详细说一下嘛,这样做有什么好处

这个有人叫它扰动函数,对hashcode的高十六位与低十六位进行了异或运算
好处有两个:

  1. 位运算,可以提高算法的运算效率
  2. 不会造成因为高位没有参与下标的计算而增加的碰撞,增加hashcode的散列性,

为什么这样设计可以增加散列性

        key.hashCode()是来自key本身hash值,本身太大,所以要与数组length-1做与运算(有0则0)。
这样操作固然可以让数变得很小,但也产生了一个问题,那就是只会取到最后几位,碰撞的几率会增大。
所以这就需要一个方法来增加要取模的数据的变动性,上面的hash中高位异或的操作就是为了解决这个问题而设计,
它将原始哈希码的高位和低位做异或混合,以此来加大低位的随机性。

LinkedHashMap怎么实现有序的

LinkedHashMap内部维护了一个单链表,有头尾节点,同时LinkedHashMap节点Entry内部除了继承HashMap的Node属性,
还有before 和 after用于标识前置节点和后置节点。可以实现按插入的顺序或访问顺序排序。 

讲一下几个主要函数的逻辑思路吧 get(),put(),resize(),replace(),remove()

涉及put&replace、remove等发生变动的操作,需要如下验证及处理:

数组是否为空——put&replace操作创建,remove操作返回

目标数组是否存在、key是否存在——数组存在则判断节点,put&replace操作(节点存在则替换,节点不存在则创建),      remove操作(节点存在则删除,节点不存在则返回)

节点存储形式是链表还是红黑树

操作完成后,是否达到扩容/缩减阀值 ——扩容/红黑树与链表转换

其他详细分析见上面主要方法分析

六、参考资料

https://blog.csdn.net/java_wxid/article/details/106896221?utm_source=app

https://zhuanlan.zhihu.com/p/21673805

https://blog.csdn.net/v123411739/article/details/78996181

https://blog.csdn.net/u012211603/article/details/79879944

https://blog.csdn.net/qq_41345773/article/details/92066554

https://blog.csdn.net/jdjdndhj/article/details/54407252

https://blog.csdn.net/u011240877/article/details/53358305

猜你喜欢

转载自blog.csdn.net/qq_36766417/article/details/109034107