• Java 语言特性【二】——Java 集合类 HashMap 解析


    引言

    Java 类库中包含了 Map 的几种实现,包括:HashMap,TreeMap,LinkedHashMap,WeakHashMap,ConcurrentHashMap,IdentityHashMap。

    下面对 HashMap 进行分析,几个问题:构造函数?如何存取?

    HashMap

    HashMap 底层是 哈希表(Hash table) 或称为 散列表,散列表的实现常常叫做 散列(hashing)。散列是一种用于以常数平均时间执行插入、删除和查找的技术。

    散列是数据结构的一种。(关于数据结构有个可视化网站,对数据结构的理解很有帮助 Data Structure Visualizations

    对 HashMap 来说,Key 不允许重复,Value 允许重复,k、v都可以是null。

    示例解析

    在开发过程中最常用的是 put 和 get 方法存取值。先来个 hello world:

    package com.xgcd.map;
    
    import java.util.HashMap;
    
    public class HashMapTest {
        public static void main(String[] args) {
            HashMap<Object, Object> map = new HashMap<>();
            map.put("abc", "123");
            System.out.println(map.get("abc"));// 123
        }
    }

    下面对这三行代码分析一下,new、put、get。

    一、new HashMap<>()

    在创建 map 对象时,new HashMap 用的是 HashMap 的无参构造方法,看源码,默认初始容量(initial capacity) 16,负载因子(load factory) 0.75 。

        /**
         * 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
        }

    初始容量默认 16,用于设置 map 对象中元素个数。

    负载因子默认 0.75,是指当 map 中元素超过容量的75%时会自动扩容(resize),扩容放到后面说。

    二、put() 方法

    进入 put() 方法,key 和 value 相关联:

        public V put(K key, V value) {
            return putVal(hash(key), key, value, false, true);
        }

    继续,进入 putVal() 方法:

        /**
         * 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
         * @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为数组,p是每个桶
            HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i;
            // 如果数组(table)为空,则调用resize()扩容创建一个数组
            if ((tab = table) == null || (n = tab.length) == 0)
                n = (tab = resize()).length;
            // 计算元素索要存储的数组下标,算法是(n-1)&hash,如果此下标没有元素则直接插入
            if ((p = tab[i = (n - 1) & hash]) == null)
                tab[i] = newNode(hash, key, value, null);
            // 如果在数组table的下标i位置已经有元素了,也就是发生了所谓的hash碰撞,有两种情况:
            // 1、key值是一样的,直接替换value值(也就是覆盖)
            // 2、key值不一样,又有两种处理方式,判断链表是否是红黑树:
                // 2.1 是红黑树,存储在红黑树中
                // 2.2 是正常的链表,则存储在i位置的链表中(直接插到最后面)
            else {
                HashMap.Node<K,V> e; K k;
                // 1、key值一样
                if (p.hash == hash &&
                        ((k = p.key) == key || (key != null && key.equals(k))))
                    e = p;
                // 2.1 是红黑树
                else if (p instanceof HashMap.TreeNode)
                    e = ((HashMap.TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
                // 2.2 不是红黑树即是链表,遍历链表
                else {
                    for (int binCount = 0; ; ++binCount) {
                        // 遍历直到链表尾端都没有找到key值相同的节点,则生成一个新的Node
                        if ((e = p.next) == null) {
                            // 创建链表节点并插入尾部
                            p.next = newNode(hash, key, value, null);
                            // 超过了链表的设置长度8则转为红黑树
                            if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                                treeifyBin(tab, hash);
                            break;
                        }
                        // 如果节点key存在,则覆盖原来位置上的key,同时将原来位置的元素沿着链表向后移动一位(也就是传说中的“头插法”)
                        if (e.hash == hash &&
                                ((k = e.key) == key || (key != null && key.equals(k))))
                            break;
                        p = e;
                    }
                }
                if (e != null) { // existing mapping for key
                    V oldValue = e.value;
                    if (!onlyIfAbsent || oldValue == null)
                        e.value = value;
                    afterNodeAccess(e);
                    return oldValue;
                }
            }
            ++modCount;
            if (++size > threshold)
                resize();
            afterNodeInsertion(evict);
            return null;
        }

    其中,table 是一个成员变量,用散装英语翻译一下:

    该 表(table) 在首次使用时初始化,并根据需要调整大小。 分配后,长度始终是2的幂。 (在某些操作中,我们还允许长度为零,以允许使用当前不需要的引导机制。)

        /**
         * 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.)
         */
        transient Node<K,V>[] table;

    查看 table 数据类型 Node<K,V>[] 发现 Node 是实现了 Map.Entry<K,V> 而 Entry 是单向链表,table 就是以 Node<K,V> 为元素的数组,这也就是称 HashMap 底层是 数组+链表 的原因。JDK1.8 后,HashMap 又引入了红黑树的数据结构。

    需要注意的是,在计算元素所在数组下标 index 时,算法是(n - 1) & hash;代码如下:

    int index = hash(key) & (capacity - 1)

    hash 值的计算是调用的 hash(Object key) 方法。

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

    可见,hash 值并不是 key 本身的 hashcode,而是一种算法。

    这里思考两个问题:

    1、为什么必须是右移16位
    首先hashcode本身是个32位整型值(int是32位)。获取对象的hashcode之后,先进行移位运算,再和自己做异或运算,非常巧妙,将高16位移到低16位,这样计算得到的整型值将“具有”高位和低位的性质。

    因为需要考虑这样的情况:有些数据计算出的hash值差异主要在高位,而hashmap里的hash寻址(也就是计算放置到数组的索引位置)是忽略容量(初始16)以上的高位的,这种处理可以有效避免类似情况的哈希碰撞。

    举个例子:我们假设有一种情况,对象A的hashCode 为 1000010001110001000001111000000,对象 B 的 hashCode 为 0111011100111000101000010100000。

    如果容量是16,16-1=15,二进制1111,对 与运算这两个数, 你会发现高位都未参与运算,结果都是0。这样的散列结果太让人失望了。很明显不是一个好的hash算法。

    但是如果我们将 hashCode 值右移 16 位,也就是取 int 类型的一半,刚好将该二进制数对半切开。并且使用位异或运算(如果两个数对应的位置相反,则结果为1,反之为0),这样的话,就能避免我们上面的情况的发生。

    2、为什么要容量减1
    最后,用hash表当前的容量减1,再和刚计算出来的整型值做位与运算,为什么要容量减1呢?

    因为A%B = A & (B-1),该式子在B是2的指数时成立,转换为取模运算,结果只取决于hash值。这也是为什么容量建议2的幂次方,这样保证&中的二进制位全是1,最大限度利用hash值,更好的散列,让hash值均匀的分布在桶中!

    另外思考两个问题,在 putVal 方法中还提到了当超过链表的设置长度时,会转为红黑树,那为什么要引入红黑树?为什么树化的临界值又是8呢?

    为什么会引入红黑树?

    引入红黑树目的是做查询优化。在平常用 HashMap 的时候,HashMap 里面存储的 key 是具有良好的 hash 算法的 key(比如String、Integer等包装类),冲突几率自然微乎其微,此时链表几乎不会转化为红黑树,但是当 key 为我们自定义的对象时,我们可能采用了

    不好的 hash 算法,使 HashMap 中 key 的冲突率极高,但是这时HashMap为了保证高速的查找效率,就引入了红黑树来优化查询了。

    为什么树化的临界值为8?

    通过源码我们得知 HashMap 源码作者通过泊松分布算出,当桶中结点个数为8时,出现的几率是亿分之6的,因此常见的情况是桶中个数小于8的情况,此时链表的查询性能和红黑树相差不多,因为转化为树还需要时间和空间,所以此时没有转化成树的必要。

    既然个数为8时发生的几率这么低,为什么还要当链表个数大于8时来树化,来优化这几乎不会发生的场景呢?

    首先我们要知道亿分之6这个几乎不可能的概率是建立在什么情况下的?答案是建立在良好的 hash 算法情况下。例如 String,Integer 等包装类的 hash 算法、一旦发生桶中元素大于8,说明是不正常情况,可能采用了冲突较大的hash算法,此时桶中个数出现超过8

    的概率是非常大的,可能有n个key冲突在同一个桶中,此时再看链表的平均查询复杂度和红黑树的时间复杂度,就知道为什么要引入红黑树了。举个例子,若hash算法写的不好,一个桶中冲突1024个 key,使用链表平均需要查询512次,但是红黑树仅仅10次,红黑

    树的引入保证了在大量 hash 冲突的情况下,HashMap 还具有良好的查询性能。

     三、get() 方法

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

    进入 getNode() 方法:

        /**
         * Implements Map.get and related methods.
         *
         * @param hash hash for key
         * @param key the key
         * @return the node, or null if none
         */
        final HashMap.Node<K,V> getNode(int hash, Object key) {
            HashMap.Node<K,V>[] tab; HashMap.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 HashMap.TreeNode)
                        return ((HashMap.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;
        }

    这里看一下 equals() 方法:

        public boolean equals(Object obj) {
            return (this == obj);
        }

    思考两个问题:

    为什么equals()方法要重写?

    判断两个对象在逻辑上是否相等,如根据类的成员变量来判断两个类的实例是否相等,而继承Object中的equals方法只能判断两个引用变量是否是同一个对象。这样我们往往需要重写equals()方法。

    我们向一个没有重复对象的集合中添加元素时,集合中存放的往往是对象,我们需要先判断集合中是否存在已知对象,这样就必须重写equals方法。

    怎样重写equals()方法?

    重写equals方法的注意点:

    1、自反性:对于任何非空引用x,x.equals(x)应该返回true。

    2、对称性:对于任何引用x和y,如果x.equals(y)返回true,那么y.equals(x)也应该返回true。

    3、传递性:对于任何引用x、y和z,如果x.equals(y)返回true,y.equals(z)返回true,那么x.equals(z)也应该返回true。

    4、一致性:如果x和y引用的对象没有发生变化,那么反复调用x.equals(y)应该返回同样的结果。

    5、非空性:对于任意非空引用x,x.equals(null)应该返回false。

    扩容机制 resize()

    插入的元素太多,数组装不下了就只能扩容了,HashMap会在原来的基础上把数组的容量增加一倍。

    当然Java里的数组是无法自动扩容的,方法就是创建一个新的更大的数组代替已有的容量小的数组。

    然后Node类的hash对数组的长度重新取余,以确定数组的下标。于是乎HashMap里元素的顺序又重排了。

    扩容:一是扩大table的长度,而是修改node的位置。容量n扩大一倍,新table中,node的下标要么还是原来的t,要么是t+n。

    HashMap有两个成员变量:

    DEFAULT_INITIAL_CAPACITY: HashMap默认的初始化数组的大小,默认为16

    DEFAULT_LOAD_FACTOR: 加载因子,默认为0.75,,当HashMap的大小达到数组的0.75的时候就会扩容。

    查看 resize() 方法代码:

    final Node<K,V>[] resize() {
        //创建一个oldTab数组用于保存之前的数组
        Node<K,V>[] oldTab = table;     
        //获取原来数组的长度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;  
        //原来数组扩容的临界值
        int oldThr = threshold;     
        int newCap, newThr = 0;
        if (oldCap > 0) {
            //如果原来的数组长度大于最大值(2^30)
            if (oldCap >= MAXIMUM_CAPACITY) {   
                //扩容临界值提高到正无穷
                threshold = Integer.MAX_VALUE;  
                //返回原来的数组,也就是系统已经管不了了
                return oldTab;      
            }
            //新数组(newCap)长度乘2 < 最大值(2^30) && (原来的数组长度) >= 初始长度(2^4)
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)    
                //这个else if中实际上就是咋判断新数组(此时刚创建还为空)和老数组的长度合法性,
                //同时交待了扩容是以2^1为单位扩容的。
                newThr = oldThr << 1; 
        }// newThr(新数组的扩容临界值)一样,在原有临界值的基础上扩2^1
        else if (oldThr > 0) // initial capacity was placed in threshold
            //新数组的初始容量设置 为老数组扩容的临界值
            newCap = oldThr;    
        // 否则 oldThr == 0,零初始阈值表示使用默认值
        else {               
            //新数组初始容量设置为默认值
            newCap = DEFAULT_INITIAL_CAPACITY;  
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        //如果newThr ==0,说明为上面 else if(oldThr > 0)的情况(其他两种情况都对newThr的值做了改变),
        //此时newCap = oldThr;
        if (newThr == 0) {  
            //ft为临时变量,用于判断阈值的合法性,
            float ft = (float)newCap * loadFactor; 
            //计算新的阈值
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE); 
        }
        //改变threshold值为新的阈值
        threshold = newThr; 
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        //改变table全局变量为扩容后的newTable
        table = newTab; 
        if (oldTab != null) {
            //遍历老数组,将老数组(或者原来的桶)迁移到新的数组(新的桶)中
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                //新建一个Node<K,V>类对象,用它来遍历整个数组。
                if ((e = oldTab[j]) != null) {  
                    oldTab[j] = null;//老的table不用了,赋值为null,垃圾回收
                    //如果e的下一个节点是null说明没有链表或树的结构,重新计算下标,赋值到新的table
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    //如果e已经是一个红黑树的元素
                    else if (e instanceof TreeNode) 
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    // 链表重排,注意,原table的某些key会被计算到同一个下标,但是新的table中不一定
                    // 因此,链表可能会拆散,变成0-2个链表
                    // 所以,定义两个node对,一个是loHead,loTail;一个是hiHead,hiTail
                    else { 
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                            do {
                                next = e.next;
                                // e.hash & oldCap==0的Node会被分配到同一个位置,确切的说,和原table下标一样
                                if ((e.hash & oldCap) == 0) {
                                    if (loTail == null)
                                        loHead = e;
                                    else
                                        loTail.next = e;
                                    loTail = e;
                                }
                                //其余节点会被分配到另一个的同一位置,确切说是原table下标+oldCap
                                else {
                                    if (hiTail == null)
                                        hiHead = e;
                                    else
                                        hiTail.next = e;
                                    hiTail = e;
                                }
                            } while ((e = next) != null);
                            // 在对应的位置上赋值
                            if (loTail != null) {
                                loTail.next = null;
                                newTab[j] = loHead;
                            }
                            if (hiTail != null) {
                                hiTail.next = null;
                                newTab[j + oldCap] = hiHead;
                            }
                        }
                    }
                }
            }
            return newTab;
    }

    这里思考扩容涉及到一个问题

    1、如何知道要将原数组的某个元素放到新数组的哪个索引位置上?

    也就是说如何确定元素e在新数组的位置。之前put的时候,用的是hash(key) & (capacity - 1)确定,为什么不继续用该方法,却转而判断(e.hash & oldCap) == 0,判断原来的元素在新数组上是否移位,假设capacity是16,只需要看倒数第五位,如果为0,下标不变,

    如果是1,下标加上容量oldCap。


    HashMap为什么是线程不安全的?

     HashMap 在并发时出现的问题可能是两方面:

    1、put的时候导致的多线程数据不一致

    比如有两个线程A和B,首先A希望插入一个key-value对到HashMap中,首先计算记录所要落到的 hash桶的索引坐标,然后获取到该桶里面的链表头结点,此时线程A的时间片用完了,而此时线程B被调度得以执行,和线程A一样执行,只不过线程B成功将记录插到了

    桶里面,假设线程A插入的记录计算出来的 hash桶索引和线程B要插入的记录计算出来的 hash桶索引是一样的,那么当线程B成功插入之后,线程A再次被调度运行时,它依然持有过期的链表头但是它对此一无所知,以至于它认为它应该这样做,如此一来就覆盖了线

    程B插入的记录,这样线程B插入的记录就凭空消失了,造成了数据不一致的行为。

    2、resize 而引起死循环

    这种情况发生在HashMap自动扩容时,当2个线程同时检测到元素个数超过 数组大小 × 负载因子。此时2个线程会在put()方法中调用了resize(),两个线程同时修改一个链表结构会产生一个循环链表(JDK1.7中,会出现resize前后元素顺序倒置的情况)。接下来再想

    通过get()获取某一个元素,就会出现死循环。

    HashMap和HashTable的区别?

    1、HashMap和Hashtable都实现了Map接口,但决定用哪一个之前先要弄清楚它们之间的分别。主要的区别有:线程安全性,同步(synchronization),以及速度。

    HashMap几乎可以等价于Hashtable,除了HashMap是非synchronized的,并可以接受null (HashMap可以接受为null的键值(key)和值(value),而Hashtable则不行)。

    2、HashMap是非synchronized,而Hashtable是synchronized,这意味着Hashtable是线程安全的,多个线程可以共享一个Hashtable;而如果没有正确的同步的话,多个线程是不能共享HashMap的。Java 5提供了ConcurrentHashMap,它是HashTable的替代,比

    HashTable的扩展性更好。

    3、另一个区别是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。这条同样也是Enumeration和Iterator的区别。

    4、由于Hashtable是线程安全的也是synchronized,所以在单线程环境下它比HashMap要慢。如果你不需要同步,只需要单一线程,那么使用HashMap性能要好过Hashtable。

    5、HashMap不能保证随着时间的推移Map中的元素次序是不变的。

    拉链法导致的链表过深,为什么不用二叉查找树代替而选择红黑树?为什么不一直使用红黑树?

    之所以选择红黑树是为了解决二叉查找树的缺陷:二叉查找树在特殊情况下会变成一条线性结构(这就跟原来使用链表结构一样了,造成层次很深的问题),遍历查找会非常慢。而红黑树在插入新数据后可能需要通过左旋、右旋、变色这些操作来保持平衡。引入红

    黑树就是为了查找数据快,解决链表查询深度的问题。我们知道红黑树属于平衡二叉树,为了保持“平衡”是需要付出代价的,但是该代价所损耗的资源要比遍历线性链表要少。所以当长度大于8的时候,会使用红黑树;如果链表长度很短的话,根本不需要引入红黑

    树,引入反而会慢。

    扩展问题简答

    table数组什么时候获得初始化?

    第一次插入元素的时候

    初始化hashMap后,第一次放入元素,table的长度是多少?

    16

    new HashMap(19),创建的map中table数组长度多大?

    初始化时实际上为null,第一次插入元素时32.

    你知道HashMap的工作原理吗?你知道HashMap的get()方法的工作原理吗?

    HashMap是基于hashing的原理,我们使用put(key, value)存储对象到HashMap中,使用get(key)从HashMap中获取对象。

    当我们给put()方法传递键和值时,我们先对键调用hashCode()方法,返回的hashCode用于找到bucket位置来储存Entry对象。

    这里关键点在于指出,HashMap是在bucket中储存键对象和值对象,作为Map.Entry。

    这一点有助于理解获取对象的逻辑。如果你没有意识到这一点,或者错误的认为仅仅只在bucket中存储值的话,你将不会回答如何从HashMap中获取对象的逻辑。这个答案相当的正确,也显示出面试者确实知道hashing以及HashMap的工作原理。

    两个hashcode相同的时候会发生什么?

    hashcode相同,bucket的位置会相同,也就是说会发生碰撞,哈希表中的结构其实有链表(LinkedList),这种冲突通过将元素储存到LinkedList中,解决碰撞。储存顺序是放在表头。

    如果两个键的hashcode相同,如何获取值对象?

    如果两个键的hashcode相同,即找到bucket位置之后,我们通过key.equals()找到链表LinkedList中正确的节点,最终找到要找的值对象。

    一些优秀的开发者会指出使用不可变的、声明作final的对象,并且采用合适的equals()和hashCode()方法的话,将会减少碰撞的发生,提高效率。不可变性使得能够缓存不同键的hashcode,这将提高整个获取对象的速度,使用String,Interger这样的wrapper类作为键

    是非常好的选择。

    如果HashMap的大小超过了负载因子(load factor)定义的容量?怎么办?

    HashMap里面默认的负载因子大小为0.75,也就是说,当一个map填满了75%的bucket时候,和其它集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作

    rehashing,因为它调用hash方法找到新的bucket位置。

    HashMap在并发执行put操作,会引起死循环,为什么?

    hashmap本身就不是线程安全的。多线程会导致hashmap的node链表形成环形链表,一旦形成环形链表,node 的next节点永远不为空,就会产生死循环获取node。从而导致CPU利用率接近100%。

    hashing的概念

    散列法(Hashing)或哈希法是一种将字符组成的字符串转换为固定长度(一般是更短长度)的数值或索引值的方法,称为散列法,也叫哈希法。由于通过更短的哈希值比用原始值进行数据库搜索更快,这种方法一般用来在数据库中建立索引并进行搜索,同时还用在

    各种解密算法中。

    为什么String, Interger这样的wrapper类适合作为键?

    因为他们一般不是不可变的,源码上面final,使用不可变类,而且重写了equals和hashcode方法,避免了键值对改写。提高HashMap性能。

    String, Interger这样的wrapper类作为HashMap的键是再适合不过了,而且String最为常用。因为String是不可变的,也是final的,而且已经重写了equals()和hashCode()方法了。其他的wrapper类也有这个特点。不可变性是必要的,因为为了要计算hashCode(),就要

    防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。不可变性还有其他的优点如线程安全。如果你可以仅仅通过将某个field声明成final就能保证hashCode是不变的,那么请这么做吧。因为获取对象的时

    候要用到equals()和hashCode()方法,那么键对象正确的重写这两个方法是非常重要的。如果两个不相等的对象返回不同的hashcode的话,那么碰撞的几率就会小些,这样就能提高HashMap的性能。

    感谢

    https://blog.csdn.net/wjl31802/article/details/89603285

    https://blog.csdn.net/qq_43519310/article/details/102887039

    HashMap方法hash()、tableSizeFor()

    https://www.jianshu.com/p/ee0de4c99f87

    https://blog.csdn.net/wufaliang003/article/details/79997585

  • 相关阅读:
    【笔记】xml文件读写
    创业唯一不需要的是金钱
    关于阻焊层和助焊层的理解
    UNIX net
    一种方便调试的打印语句宏定义
    C语言指针一种容易错误使用的方法
    文件操作
    MPEG文件格式
    指针在函数间传递实质
    如何查看静态库内容 Unix/Linux
  • 原文地址:https://www.cnblogs.com/yadongliang/p/12333961.html
Copyright © 2020-2023  润新知