• HashMap核心功能源码浅析


    1.引子

    "HashMap"由“hash”和“map"两个单词组成,这里的”map"表示“映射”而不是“地图”的意思,两个单词连起来就是“哈希映射表”。Map是一个接口,它有TreeSet 、LinkedHashMap、EnumMap、HashMap等实现类,其中HashMap无疑最重要也很复杂的一个实现类。理清楚了HashMap的原理,对其他的Map实现类也就触类旁通了,其他Map实现类大部分都相对简单.

    哈希是一种算法也是一种数据结构。哈希算法能够根据给定的键key快速定位到对应的值value ,HashMap是利用哈希算法实现的一个数据结构。

    散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

     HashMap内部常用到Object的"hashCode()"和"equal(Object)",要使用HashMap这种集合类型必须重写元素的这两个方法。

     

    2.HashMap的结构

    1).类结构

    HashMap继承AbstractMap,实现了Map接口,Map接口中的”addAll()“ 、"containsAll()"等方法由父类AbstractMap实现。

    静态内部类Node实现了Map.Entry接口(Map中的内部接口),它用来保存一个键-值对,表示一个链表类型节点。而TreeNode扩展了Node,当哈希人冲突严重的时候,使用TreeNode保存健单个键-值对,表示一个二叉树节点。

    内部类EntrySet表示HashMap中的entry集合,KeySet表示HashMap的所有Key集合,Values表示所有的value集合。

    EntrySet 、KeySet 、Values均是成员内部类而不量静态内部类,这是因为这3个内部类均要直接访问外部部类HashMap的相关成员变量,它们必须与外部类HashMap相绑定。

     

    2).数据结构

    HashMap的基本数据结构是"数组+链表(或红黑树)",数组table是顶级容器,它是哈希桶的容器,而哈希桶又是(存放键值对)节点的容器。当哈希桶所含的节点个数达到相应的阀值时,哈希桶的数据结构会进行相应的变化,即单向链表和红黑树进行相互转化。

     

     

    3.内部组成

    1).静态内部类Node

    Node表示包含一个键-值对基础的哈希节点。

    一个Node实例表示单向链表的一个节点,当这个实例是头节点时,这个实例又可以直接表示整个链表。

    Node类中有4个成员变量:

    final int hash;
    final K key;
    V value;
    Node<K,V> next;

    key value分别表示当前节点的键和值

    next表示下个节点的引用(默认情况下使用链表处理哈希冲突,所以必须要有后继节点的引用)

    hash表示将key进行哈希运算后的结果(使用HashMap的静态方法"hash(Object)"计算),此属性非常重要,它决定当前节点放在哈希表的哪个桶上。

    2).静态内部类TreeNode

    TreeNode也表示包含一个键-值对基础的哈希节点,一个红黑树节点,由于它间接继承上面提到的Node,因此它也可以表示为一个单向链表节点。

    当一个桶上的节点少于8个时,使用Node当作哈希节点,当一个桶上的节点多于8个时,即哈希冲突严重时,使用TreeNode。

    TreeNode中有5个成员变量。

    parent、left 、right 、prev分别表示父节点、左节点、右节点、前节点

    red表示当前节点的红/黑点

            TreeNode<K,V> parent;  // red-black tree links
            TreeNode<K,V> left;
            TreeNode<K,V> right;
            TreeNode<K,V> prev;    // needed to unlink next upon deletion
            boolean red;

    3).常量与成员变量

    主要有这几个静态常量

    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 
    static final int MAXIMUM_CAPACITY = 1 << 30;
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    static final int TREEIFY_THRESHOLD = 8;
    static final int UNTREEIFY_THRESHOLD = 6;
    static final int MIN_TREEIFY_CAPACITY = 64;

    DEFAULT_INITIAL_CAPACITY为映射表的最默认容量,值为16,无参构造方法使用这个容量。

    MAXIMUM_CAPACITY为映射表的最大容量,一般情况下容量不会超过这个数。

    DEFAULT_LOAD_FACTOR是默认的加载因子,无参构造方法使用这个加载因子。

    TREEIFY_THRESHOLD和UNTREEIFY_THRESHOLD分别是从链表转为红黑树和红黑树转为链表的单个桶节点个数阀值。一个桶上链表节点超8个就将其为红黑树,当一个桶上红黑树结点少于6个就将其转为链表。

    MIN_TREEIFY_CAPACITY是从链表转为红黑树时所要求的tabel最小长度(只有同时满足TREEIFY_THRESHOLD和MIN_TREEIFY_CAPACITY时才能从链表转化为红黑树)。

     

    成员变量

    transient Node<K,V>[] table;
    transient Set<Map.Entry<K,V>> entrySet;
    transient int size;
    transient int modCount;
    int threshold;
    final float loadFactor;

     table是Node类型的数组,表示哈希桶。其中的每个元素是一个单向链表或红黑树。

    entrySet是HashMap的所有Entry的集合。

    size表示键值对的个数。

    modCount表示当前实例被修改(添加或删除键值对)的次数,这用来作为快速失败的参考依据。

    loadFactor表示加载因子,元素个数与容量的比值的达到这个值,table将扩容。

    threshold表示阀值,它是容量与加载因子的乘积,即threshold=loadFactor*capacity。当键值对的个数size大于它时,table将扩容。

     

    4.实现原理

    构造方法

    有一个无参构造方法,3个带参构造方法

      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; // all other fields defaulted
        }
    
       
        public HashMap(Map<? extends K, ? extends V> m) {
            this.loadFactor = DEFAULT_LOAD_FACTOR;
            putMapEntries(m, false);
        }

    无参构造方法”HashMap()“,使用默认的容量16、默认的加载因子0.75.

    带参构造方法“HashMap(int , float )”,根据参考设写初始容量和加载因子。

    带参构造方法“ HashMap(Map)”,将一个Map中的所有键值 对添加到新的HashMap中,并使用默认的加载因子。

    1).保存键值对put(K,V)

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

    put(K,V)方法先使用hash(O)方法,算出k对应的hash值,然后调用putVal()方法进行实际的键值对插入。

    其实"java.util.HashMap#hash(Object)"方法主要逻辑很简单,就是将key.hashCode()的返回值和此返回值的无符号右移16位结果进行按位异或运算。

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

     

     根据位操作特点”(1或0)与0进行按位异或运算的结果是其本身“可看出,此处哈希算法的实质是,hashCode的高16位同样作为hash的高16位,将hashCode的高16位和其低16位的异或运算结果作为hash的低16位。

     实际上putIfAbsent(K,V)也调用了putVal()方法,putVal是一个很重要也很复杂的一个方法。

    putVal的核心逻辑:先根据hash确定当前键值应放入哈希表的哪个桶(这个桶是一个链表或红黑树),然后再此基础上再判断这个桶上是否存在当前键值对就存入的节点,若存在这样的节点,只需要将此节点的value属性更新为当前要添加的value.反之就要构建一个新的节点,并将此节点添加在这个桶上。

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        //tab表示哈希桶数组,p表示当前键值对应存入的链表或红黑树(桶)
        // n表示table的长度,i表示数组tab的下标
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            //table为空或没有元素时需要重新调整table的大小,否则无法容纳此时新添加的键值
            n = (tab = resize()).length;
        /**
         * ”(n - 1) & hash“是当前键值对应该落入哈希桶的下标,这是个取余的表示式。
         * 根据位运算的特点,只要n是2的幂次方,表达式"hash % n"与”(n - 1) & hash“等价
         */
        if ((p = tab[i = (n - 1) & hash]) == null)
            //此下标对应的链表或红黑树为空,将它初始化
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;//e表示当前键值对应存入的节点
            if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                //若链表的头节点(若是红黑树结构就是根节点)的key与当前要添加的键值对key的equals和hash均相同,
                //那么此链表的头节点就是当前键值要存入的节点
                e = p;
            else if (p instanceof TreeNode)
                //若是红黑树结构就,调用单独的方法添加键值对
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                //当前键值对应存入的节点在链表的非头节点位置,
                //需要在链表上从头到尾进行遍历,检查此链表上是否已存在key对应的节点。
                // 若存在这样的节点,将此节点的value替换成要添加的即可
                //若不存在这样的节点,需要构建一个新节点,将此节点添加在链表的尾部
                for (int binCount = 0; ; ++binCount) {//binCount表示当前单向链表上的节点个数。
                    //e表示p的后继节点,在这里可以理解为:p表示e的前驱节点(单向链表不存在前驱节点的说法)
                    if ((e = p.next) == null) {
                        //已经遍历到尾部了,链表上没有这样的节点,必须构建一个新节点放在链表尾部
                        p.next = newNode(hash, key, value, null);
                       // 这里需要TREEIFY_THRESHOLD,因为即将添加一个节点,在节点添加成功后,就会有TREEIFY_THRESHOLD个键值对
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st 
                            //达到阀值了,需要将链表转化为红黑树
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        break;//链表上存在key对应的的节点
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                //链表上存在key对应的的节点
                V oldValue = e.value;//保存原来的value,最后方法需要返回这个值
                /**
                 *  HashMap.putIfAbsent方法只有map中不存在key对应节点将才添加键值对,
                 *  调用当前putVal()方法传入的onlyIfAbsent参数为true,
                 *
                 *  进入此处就表明map中已存在key对应的节点了(前面if条件中"e!=null"成立),
                 *  所以这里要在if条件中对onlyIfAbsent取反。
                 */
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);//空方法,留给子类LinkedHashMap等实现,进而读取有序的功能
                return oldValue;
            }
        }
        //map中不存在key对应的节点,map会添加新节点,修改次数自增1
        ++modCount;
        if (++size > threshold) //添加了新节点,键值对个数加1
            //如果键值对个数超过阀值,需要重新调整table的容量。
            resize();
        afterNodeInsertion(evict);//空方法,留给子类LinkedHashMap等实现,进而实现添加(插入)有序的功能
        return null;
    }

    putVal()逻辑详细分析:

    1 .如果第1次添加键值对,table应该是null,resize()方法将对table分配内存扩容。

    接下来分析下resize()方法如何扩容的:

       final Node<K,V>[] resize() {
           Node<K,V>[] oldTab = table;
           int oldCap = (oldTab == null) ? 0 : oldTab.length;
           int oldThr = threshold;
           int newCap, newThr = 0;
           if (oldCap > 0) {
               if (oldCap >= MAXIMUM_CAPACITY) {
                   //原容量过大,不能再扩容了,将阀值设为int类型的最大值,返回原table
                    threshold = Integer.MAX_VALUE;
                   return oldTab;
               }
               else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                       oldCap >= DEFAULT_INITIAL_CAPACITY)
                   //扩容(原容量的2倍)后小于MAXIMUM_CAPACITY 并且原容量大于默认初始化阀值,
                   //阀值设为原来的2倍
                   newThr = oldThr << 1; // double threshold
           }
           else if (oldThr > 0) // initial capacity was placed in threshold
               //原容量为0且原阀值大于0,
               newCap = oldThr;
           else {               // zero initial threshold signifies using defaults
               //阀值为0,容量为0,使用默认的容量,
               newCap = DEFAULT_INITIAL_CAPACITY;
               newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//loadFactor*capacity
           }
           if (newThr == 0) {
               //新阀值为0时,需要重新调整阀值
               float ft = (float)newCap * loadFactor; //loadFactor*capacity
               //为了使阀值不超出限定范围
               newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                       (int)ft : Integer.MAX_VALUE);
           }
           threshold = newThr;
           @SuppressWarnings({"rawtypes","unchecked"})
           Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//重新分配数组大小
           table = newTab;
           if (oldTab != null) {
               //将原数组oldTab的元素复制到新数组newTab中
               for (int j = 0; j < oldCap; ++j) {//遍历原数组oldTab
                   Node<K,V> e;//e用来保存原数组中每元素
                   if ((e = oldTab[j]) != null) {
                       oldTab[j] = null;//将原数组的元素引用赋null,加快垃圾回收
                       if (e.next == null)
                           //e的后继节点为null,e所表示的链表(或红黑树)上只有e这一个节点。
                           //可以直接算出此链表(或红黑树)e在新数组中应放置的下标,然后再将下标对应的元素赋值
                           newTab[e.hash & (newCap - 1)] = e;
                       else if (e instanceof TreeNode)//e是红黑树,单独处理
                           ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                       else { // preserve order
                           // e是单向链表,且链表上有多个节点时
                           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构造一条链表,小索引链表
                               // 此链表放在新table的j索引位置
                               if ((e.hash & oldCap) == 0) {
                                   if (loTail == null)
                                       loHead = e;//loHead的引用只初始化一次,以后不再变化
                                   else
                                       loTail.next = e;
                                   loTail = e;
                               }
                               //(e.hash & oldCap)!=0构造另一条链表,大索引链表
                               // 此链表放在新table的(j+oldCap)索引位置
                               else {
                                   if (hiTail == null)
                                       hiHead = e;//hiHead的引用只初始化一次,以后不再变化
                                   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;
       }

    resize()方法扩容时,新的容量是原容量的2倍,且原容量就是2的幂次方,也就是说HashMap的容量始终是2的幂次方。这主要是为了快速取余。只要n是2的幂次方,表达式"hash % n"与”(n - 1) & hash“等价,而位运算显然速度更快。

    在扩容过程中新旧哈希表间的元素(当哈希桶是链表结果,每个哈希表元素就是一个链表)需要复制,当链表上存在多个节点时对链表上节点的复制做了进一步的处理,根据条件语句“(e.hash & oldCap) == 0”的结果进入不同分支处理。如果链表的节点执行条件语句“(e.hash & oldCap) == 0”,其结果true、 false都有,resize方法就会构建两个链表,两个链表在数组table上索引相差oldCap。

    (2) 如果根据表达式“ (n - 1) & hash”算出键值对当前键值对应该存入哈希桶的下标,如果此哈希桶为null,就用newNode()方法将其初始化。

    newNode()只是简单地调用了Node的构造方法而已。

    Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
        return new Node<>(hash, key, value, next);
    }

    (3)在找到哈希桶后,下一步就是确定当前键值对应该丰入哈希桶后的哪个节点位置。

    此时有两种情况,一种是此桶上已经存这样的键了,另一种是此桶上不在这样的键的节点。若桶上存在这样的节点,就直接将该Node的value属性替换。若桶上不存在这样的节点,将构建一个新的Node节点,并添加在(此桶为链表结构时)链表的尾部。

    是否存在这样的键的判断流程是“先判断hash是否相等,若hash不等再判断equals()是否为true”。这里为会把先判断hash呢?因为hash已经是一个整数了,整数的比较很快速,而equals()方法需要进一步计算才能得出结果。若hash不等,就不需要进行耗时的equals方法计算,这样可以提高算法的性能。

    if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))

    2).根据键获取值get(Object)

    get(Object)方法体内部,主要调用getNode(int,Object)方法找出key对应的Node节点,然后(这个node存在时)再取node节点的value属性

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

    我们之前有过分析添加键值对的方法put(K,V)的基础 ,这里再看getNode(int,Object)方法的逻辑就很简单了。

    先确定key对应的Node节点应该在的哈希桶,再在这个哈希桶上遍历链表或红黑树,找到满足条件的key对应节点。若没有这个节点就返回空。

    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
                (first = tab[(n - 1) & hash]) != null) {//key对应的Node节点应在table的某个哈希桶。
            if (first.hash == hash && // always check first node 
                    ((k = first.key) == key || (key != null && key.equals(k))))
                //key对应的节点是哈希桶的头(根)节点,可以直接返回这个节点。
                return first;
            if ((e = first.next) != null) {
                //key对应的节点在桶上的非头(根)节点位置,需要遍历链表(红黑树)
                if (first instanceof TreeNode)//桶的数据结构是红黑树时需要单独处理
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {//遍历链表查找
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

    3).移除键值对

    ”remove(Object)“与“remove(Object, Object)”方法均能将键值对(Entry节点)从哈希映射表中移除,且这两个方法都是委托”romveNode(int,Object,Object,boolean,boolean)“方法实现的,只不过后者“remove(Object, Object)”方法要在均匹配的情况下才会移除键值对(Entry节点)。

        public V remove(Object key) {
            Node<K,V> e;
            return (e = removeNode(hash(key), key, null, false, true)) == null ?
                null : e.value;
        }
        public boolean remove(Object key, Object value) {
            return removeNode(hash(key), key, value, true, true) != null;
        }

    有了前面分析putVal方法的基础,阅读方法removeNode方法就比较轻车熟路了。

    先是根据hash找到table的索引,再遍历此索引位置元素所代表的链表(或红黑树)。尝试找到这个键值对对应的节点,判断是否找到的规则,依然是先比较hash再比较equals. 若找到这个节点,则将此节点的前后节点通过next属性链接起来。

    final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        //start 与putVal方法前半部分逻辑几乎一致
        if ((tab = table) != null && (n = tab.length) > 0 &&
                (p = tab[index = (n - 1) & hash]) != null) {
            Node<K,V> node = null, e; K k; V v;
            if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            else if ((e = p.next) != null) {
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {
                   // p表示e的前驱节点
                    do {
                        if (e.hash == hash &&
                                ((k = e.key) == key ||
                                        (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
        //end  与putVal方法前半部分逻辑几乎一致
    
            if (node != null && (!matchValue || (v = node.value) == value ||
                    (value != null && value.equals(v)))) {
                //存在这样的node节点,且未要求匹配value或在要求必须匹配value时value也刚好匹配
                if (node instanceof TreeNode)
                    //红黑树单独处理
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                else if (node == p)
                    //要移除的node节点是链表的头节点时,将原头节点的后继节点设为新的头节点
                    tab[index] = node.next;
                else
                    //要移除的node节点是非头节点时,使用next属性将node的前驱节点和后继节点直接链接在一起
                    p.next = node.next;
                ++modCount;//减少了一个节点,修改次数加1
                --size;//键值对个数自减
                afterNodeRemoval(node);//空方法,留给子类实现
                return node;
            }
        }
        return null;
    }

    4).与之相关的HashSet

    Set表示的是无重复元素且不保证顺序的集合接口。HashMap基本特性之一是Key必须保证唯一、不重复,否则无法根据Key来定位Value,正是基于这一点,HashSet实际上是利用HashMap实现的,它的大部分方法都是直接委托相应的HashMap方法完成功能。

    HashMap中有键值对,而HashSet只利用了HashMap的键。HashSet添加元素e时,会在其HashMap类型的属性map中添加键是e值o为PRESENT的键值对 ,即每次添加键值对Entry时,其值始终是常量PRESENT,只有键会根据添加的元素e不同而变化。

     

     

    5).与之相关的LinkedHashMap

    LinkedHashMap是HashMap的子类,它们保证键值对先后顺序,并且(使用构造方法相应的参数)支持选择使用访问访问添加顺序,默认使用添加顺序

    构造方法:

    其构造方法的逻辑也主要是调用父类HashMap的构造方法

    public LinkedHashMap() {
        super();
        accessOrder = false;
    }
    
    public LinkedHashMap(Map<? extends K, ? extends V> m) {
        super();
        accessOrder = false;
        putMapEntries(m, false);
    }
    
    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

    成员变量

    static class Entry<K,V> extends HashMap.Node<K,V> {
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }
    
    transient LinkedHashMap.Entry<K,V> head;
    
    transient LinkedHashMap.Entry<K,V> tail;
    
    final boolean accessOrder;

    LinkedHashMap比HashMap多一条双向链表,用此链表来记录相应的顺序。

    双向链表的类类型LinkedHashMap.Entry也是继承于父类HashMap中的静态内部类HashMap.Node<K,V>,只不过添加了before、after这两个表示前驱、后继节点的成员变量,将父类的单向链表变成了双向链表而已。

    5.总结

    1)不论是添加键值对、删除键值对或获取键对应的值,其基本流程都是先确定算出Key的hash,再根据hash找到对应的table索引(即找到哈希桶),再确定节点应在哈希桶的哪个位置。

    2)键的hash是随机的,这导致表达式“(n - 1) & hash”的结果是无规律可循的,因此HashMap的键值对是无序的。若要使用有序的HashMap,请使用其子类LinkedHashMap.

    3) 哈希表table每次扩容后,键值对Entry所在的哈希桶的索引,可能不变,即就是原来的index ,也有可能变成(index+oldCap),且只有这两种可能的情况。

    4)HashMap不是线程安全的,在并发场景建议使用ConcurrentHashMap。虽然Hashtable也是线程安全的,但它是直接在方法上使用synchronized,是利用阻塞式的锁保证线程安全的,其并发效率低。

     

  • 相关阅读:
    Apache与Nginx的优缺点比较
    [PHP基础]有关isset empty 函数的面试题
    PHP求解一个值是否为质数
    15个魔术方法的总结
    对象在类中的存储方式有哪些?
    cookie大小
    Tp3.2 和 Tp5.0之间的区别
    经典的面试题,(这是著名的约瑟夫环问题)
    怎么计算数据库有多大的数据量
    [置顶] 实用电子电路设计丛书
  • 原文地址:https://www.cnblogs.com/gocode/p/analysis-source-code-of-HashMap.html
Copyright © 2020-2023  润新知