• HashMap源码阅读笔记


    HashMap

      允许键和值为null,存储无序,在非同步和键值非null情况下相当于HashTableHashMap线程不安全,如果多个线程同时访问哈希映射,并且至少有一个线程在结构上修改了该映射,则必须在外部对其进行同步。(结构修改是添加或删除一个或多个映射的任何操作;仅更改与实例已有的键相关联的值不是结构修改)。同步例如:Map m = Collections.synchronizedMap(new HashMap(...)); 

    常用方法

            HashMap<String,String> map = new HashMap<>();
            map.put("1","a");   // 放置指定键值到map中,已存在则覆盖
    map.put("1","A"); // 若键已存在则覆盖并返回旧值(返回a) map.put("2","B"); map.put("3","C"); map.put(null,null); // 键值都可以为null,始终会被存到数组的0索引处 map.get("1"); // 获取指定键对应的值 map.keySet(); // 返回键的集合 map.values(); // 返回值的集合 map.isEmpty(); // 判断是否为空 map.size(); // 返回键值对个数 map.containsKey("1"); // 判断是否存在指定键 map.containsValue("A");// 判断是都存在指定值 HashMap<String,String> ano_map = new HashMap<>(); ano_map.put("4","D"); ano_map.put("5","E"); map.putAll(ano_map); map.remove("5"); // 移除指定键的键值对 map.replace("1","a"); // 替换指定键的值 map.replace("2","B","b"); // 迭代方式一 for (String s : map.keySet()) { // 遍历key System.out.println(s); } for (String value : map.values()) { // 遍历value System.out.println(value); } // 迭代方式二 Set<Map.Entry<String, String>> entries = map.entrySet(); Iterator<Map.Entry<String, String>> iterator = entries.iterator(); while(iterator.hasNext()){ Map.Entry<String, String> next = iterator.next(); System.out.println(next.getKey()+"---"+next.getValue()); } // 迭代方式三 Set<String> keys = map.keySet(); for (String key : keys) { Object value = map.get(key); System.out.println(key+"---"+value); } // 迭代方式四 map.forEach((key,value)->System.out.println(key+"---"+value));

    JDK1.8

    底层采用数组+链表+红黑树的方式存储。

    红黑树

    性质:

    1.每个结点只能是红色或者黑色

    2.根结点一定是黑色

    3.红色结点不连在一起

    4.每个红色结点的两个子结点必须都是黑色。

    5.叶子结点都是黑色(NULL结点,一般不画

    红黑树本质上是满足以上性质的自平衡二叉查找树不会出现退化成单支的情况

    红黑树的自平衡

    1.左旋:

    2.右旋:

    结点的插入

    插入的结点默认为红色

    1.插入6

    2.变色:

    当前结点(6)的父亲结点(7)是红色,且叔叔结点(13)也是红色:

    1.把父亲和叔叔结点都变为黑色(713

    2.把爷爷结点变为红色(12

    3.此时爷爷结点称为考察对象(12

    3.左旋:

    当前结点(12)的父结点是红色(5),叔叔结点(30)是黑色,且当前结点是右子树,以父结点左旋。得到如下:

    4.右旋:

    当前结点(5)的父亲结点(12)是红色,叔叔(30)是黑色,且当前结点是左子树,

    把父亲结点变成黑色,把爷爷结点变成红色,以爷爷结点右旋。

    JDK1.8源码阅读

    继承了AbstractMap<K,V>,实现类Map<K,V>, Cloneable, Serializable接口。

    AbstractMap<K,V>已经实现了Map<K,V>接口,这里HashMap继承了AbstractMap<K,V>,却又实现了Map<K,V>,这里是版本迭代造成的问题,是作者的失误。

    字段

    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;  // 默认初始容量(必须是2的次幂)
                                                         // 1左移4位,即16
    
    static final int MAXIMUM_CAPACITY = 1 << 30;     // 最大容量2的30次方
    
    static final float DEFAULT_LOAD_FACTOR = 0.75f;  // 默认负载因子
    
    static final int TREEIFY_THRESHOLD = 8;          // 树化阈值为8(超过8用红黑树存储)
    
    static final int UNTREEIFY_THRESHOLD = 6;        // 解除树化阈值6(小于6恢复链表存储)
    
    static final int MIN_TREEIFY_CAPACITY = 64;      // 最小树化容量64
    
    // Node数组长度总是2的幂。哈希桶数组在首次使用(put方法)时初始化,并根据需要
    // 调整大小。实际上在putVal方法中调用resize方法进行数组初始化
    transient Node<K,V>[] table;              // transient 表示序列化时该变量不参与
    
    transient Set<Map.Entry<K,V>> entrySet;   // 用于转化为Set存储
        
    transient int size;        // 键值映射数量
    
    transient int modCount;    // 结构性修改的次数
    
    int threshold;             // 阈值
    
    final float loadFactor;    // 负载因子

    构造器

        // 指定初始容量和负载因子
        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))  // 负载因子小于0或是不个数
                throw new IllegalArgumentException("Illegal load factor: " +
                                                   loadFactor);
            // 否则,赋值操作
            this.loadFactor = loadFactor;
            // threshold初始化为大于等于initialCapacity最小的2的n次幂
            // 真正的值会在putVal中调用resize方法进行修改
            this.threshold = tableSizeFor(initialCapacity); 
        }
    
        // 建议指定容量(阿里手册建议:需要存储的元素个数/负载因子+1)
        public HashMap(int initialCapacity) {    // 指定初始容量,负载因子用默认0.75
            this(initialCapacity, DEFAULT_LOAD_FACTOR);
        }
    
        public HashMap() {                    // 默认初始容量为16,负载因子为0.75  
             this.loadFactor = DEFAULT_LOAD_FACTOR;
        }
    
        public HashMap(Map<? extends K, ? extends V> m) { // 指定Map实现类对象初始化
            this.loadFactor = DEFAULT_LOAD_FACTOR;
            putMapEntries(m, false);
        }

    结点类(内部类)

        // 内部类Node实现了Map接口的内部接口Entry
        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() {
                // 实现Map接口的内部接口Entry的hashCode()方法
                return Objects.hashCode(key) ^ Objects.hashCode(value);
            }
    
            public final V setValue(V newValue) {   // 设置结点新值,返回旧值
                V oldValue = value;
                value = newValue;
                return oldValue;
            }
    
            public final boolean equals(Object o) {  // 判断是否相等
                if (o == this)
                    return true;
                if (o instanceof Map.Entry) {
                    Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                    if (Objects.equals(key, e.getKey()) &&
                        Objects.equals(value, e.getValue())) // 键和值都相同
                        return true;
                }
                return false;
            }
        }

    重点方法

        // 将指定Map实现类的对象存入    
         final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
            int s = m.size();
            if (s > 0) {
                if (table == null) { 
              // 加1后强转为int相当于向上取整,创建时就保证更大容量,防止刚创建完,还没put几个元素就要立马要扩容
              // 若不加1,若size是6,则ft是8,创建的数组正好是满的,下次    
              // put就立马要进行扩容,降低效率
                    float ft = ((float)s / loadFactor) + 1.0F;  
                    int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                             (int)ft : MAXIMUM_CAPACITY);
                    if (t > threshold)
                // threshold初始化为大于等于initialCapacity最小的2的n次幂
                // 真正的值会在putVal中调用resize方法进行修改
                        threshold = tableSizeFor(t);  
                }
                else if (s > threshold) 
                    resize();
           // 依次存入键值对
                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);
                }
            }
        }
      // 计算key的哈希值
        static final int hash(Object key) {   
            int h;
            return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
        //  key为空则hash值为0
        //  h与h无符号右移16位进行异或运算(将高16位也利用上使hash值更加随机)
        }

    hashCode()方法来自Object类,是native方法(本地方法)。

    (h = key.hashCode()) ^ (h >>> 16)的作用

    1)进行无符号右移16位后再异或运算

    注:这里的(n-1) & hashputVal方法中代码,用于计算对应的数组索引。

    实际上n在绝大多数情况都是小于216次方的,而n还必须是2的次方,则(n-1)低位连续都是1,所以(n-1) & hash也就是使hash的对应低位有效,其他高位清零。将keyhashCode方法计算的值的高16位也参与运算,可以使计算的索引更加随机,减少在hashCode方法计算的值低位不变,仅高位不同的时候造成的哈希冲突。

    2)不进行无符号右移16位后再异或运算

    如果经hashCode方法计算的值低位不变,仅仅高位不同。

    这里两次hashCode计算的值仅低位不变,两次(n-1) & hash的结算就是一样的,造成了哈希冲突。

        // 返回一个大于等于cap且是2的n次幂的table长度
      // cap为127返回128(2的7次方),cap为129返回256(2的8次方)
        static final int tableSizeFor(int cap) { 
            int n = cap - 1;    // 不减一,若cap已经是2的次幂,返回的是cap的2倍! 
            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;
        }

        public V get(Object key) {       // 根据键获取对应值
            Node<K,V> e;             // 获取对应结点
        // 根据key的hash值和key获取对应的结点
            return (e = getNode(hash(key), key)) == null ? null : e.value;
        }
    
      // 键已存在则覆盖
        public V put(K key, V value) {
            return putVal(hash(key), key, value, false, true);
        }
    // onlyIfAbsent为true代表不更改现有值,evict为false代表table为创建状态
    // 插入数据成功之后扩容
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                       boolean evict) {
            Node<K,V>[] tab; Node<K,V> p; int n, i;
            if ((tab = table) == null || (n = tab.length) == 0) // 哈希桶数组为空
                n = (tab = resize()).length;
            // 计算元素对应的数组索引,并使p指向该位置
            if ((p = tab[i = (n - 1) & hash]) == null) // 对应索引处为空
            // 利用newNode方法创建新结点存入对应的索引处
                tab[i] = newNode(hash, key, value, null); 
            else {                              // 对应索引处为非空,即冲突
                Node<K,V> e; K k;
                if (p.hash == hash &&              // 与链表首个元素的键相同
                    ((k = p.key) == key || (key != null && key.equals(k))))
                    e = p;                      // e指向该位置 
                else if (p instanceof TreeNode)   // 冲突位置已经用红黑树进行存储
                    e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
                else {                          // 冲突位置仍用链表进行存储
                    for (int binCount = 0; ; ++binCount) {  // 遍历链表
                        if ((e = p.next) == null) {    // 当前链表只有一个元素,e指向null
                            p.next = newNode(hash, key, value, null);   // 插入尾部
                            if (binCount >= TREEIFY_THRESHOLD - 1)  // 结点数大于8
                                treeifyBin(tab, hash);                // 转化为红黑树
                            break;            // 跳出链表遍历(因为已经转为红黑树了)
                        }
                        if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                  // 遍历过程中发现相同键
                            break;
                        p = e;   // p指向下一个结点
                    }
                }
                if (e != null) {     // e不为null
                    V oldValue = e.value;
              // onlyIfAbsent 为true则不更改现有值
              // 指定更改现有值或者旧值是null
                    if (!onlyIfAbsent || oldValue == null) 
                        e.value = value;
                    afterNodeAccess(e);
                    return oldValue;     // 返回旧值
                }
            }
            ++modCount;
            if (++size > threshold)       // 超出阈值
                resize();                // 扩容
            afterNodeInsertion(evict);
            return null;
        }
      // 将链表转化为红黑树
        final void treeifyBin(Node<K,V>[] tab, int hash) {
            int n, index; Node<K,V> e;
             // 数组为空或数组长度小于64
            if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
                resize();                                  // 不转化为红黑树,而是扩容
          // 根据hash值和数组长度计算对应的数组索引,e指向第一个元素
            else if ((e = tab[index = (n - 1) & hash]) != null) {  // 对应桶中元素不为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);        // 自平衡
            }
        }

    链表长度到了8,但数组长度没有到64,选择扩容解决。

    JDK1.8之前扩容时会重新进行哈希值计算,会遍历所有元素,很耗时。

    JDK1.8开始,不重新进行hash值计算,每次扩容都是翻倍,与原来计算的(n-1) & hash的结果相比,只是多了一个bit位,所以扩容后结点的位置:

    1)在原来位置;

    2)在“原来位置+旧容量”的位置。

     

    翻倍的意义在于使对应的n-1的二进制看起来就像向左推进了一个“1”。

    因此不必再重新进行hash值的计算,因为是&运算,只需看原来的hash值新增的那一位是1还是0,是0的话索引不变,是1的话变为原索引+旧容量。这样的话,扩容之后,一个桶中的元素会随机的分散到原索引或原索引+旧容量,扩容之后不会出现更严重的哈希冲突的情况。

      // 扩容(2倍)或初始化table数组
        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) { // 已达最大容量不能扩了
                    threshold = Integer.MAX_VALUE; // 阈值改成最大
                    return oldTab;                 // 返回旧数组
                }
            // 新容量=旧容量的2倍,新容量小于最大值,并且旧容量大于等于16
                else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                         oldCap >= DEFAULT_INITIAL_CAPACITY) 
                    newThr = oldThr << 1;  // 新阈值=旧阈值2倍
            } 
            else if (oldThr > 0)    // 创建时的阈值就是容量值
                newCap = oldThr;
            else {          // oldThr =0,即构造时没有指定容量,则容量、阈值使用默认值
                newCap = DEFAULT_INITIAL_CAPACITY;
                newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
            }
            if (newThr == 0) {    // 数组为空,阈值为0,这时真正计算阈值
                float ft = (float)newCap * loadFactor;
                newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                          (int)ft : Integer.MAX_VALUE);
            }
            threshold = newThr;   // 新数组阈值
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; // 创建新数组
            table = newTab;      //  table数组初始化或扩容后的数组
            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)   // 链表只有一个结点
                            newTab[e.hash & (newCap - 1)] = e; // 存入新数组
                        else if (e instanceof TreeNode) // 此处是红黑树存储
                    // 树结点拆分
                            ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                        else {  // 不为空不是红黑树,则是链表(保持顺序)
                            Node<K,V> loHead = null, loTail = null;
                            Node<K,V> hiHead = null, hiTail = null;
                            Node<K,V> next;
                            do {
                                next = e.next; 
                      // oldCap不减1,相当于检测原hash的新增为是0还是1
                                if ((e.hash & oldCap) == 0) {  // 新增位为0,即索引不变
                                    if (loTail == null)         
                                        loHead = e;
                                    else
                                        loTail.next = e;
                                    loTail = e;
                                }
                                else {            // 新增位为1,新索引=原索引+旧容量
                                    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;
        }

    JDK1.7

    字段

    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 Entry<?,?>[] EMPTY_TABLE = {};           // Entry数组
    
    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;  // Entry桶数组
    
    transient int size;
    
    transient int size;
    
    final float loadFactor;
    
    transient int modCount;
    
    static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
    
    transient int hashSeed = 0;

    构造器

        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;
        // 初始时,阈值都是容量值,在put方法调用inflateTable方法中重新计算
            threshold = initialCapacity;  
            init();
        }
    
        public HashMap(int initialCapacity) {
            this(initialCapacity, DEFAULT_LOAD_FACTOR);
        }
    
        public HashMap() {
            this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
        }
    
        public HashMap(Map<? extends K, ? extends V> m) {
            this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                          DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
            inflateTable(threshold); // 初始化table数组,并重新计算阈值
            putAllForCreate(m);
        }

    重要方法

      // 添加元素,冲突时添加到链表采用的是头插法,易产生循环链表。
      // 先扩容再插入(在addEntry方法中)
        public V put(K key, V value) {
            if (table == EMPTY_TABLE) {  // 数组是空的
                inflateTable(threshold);  // 转化为大于等于threshold的最小2次方
            }
            if (key == null)             // 键是null
                return putForNullKey(value);
            int hash = hash(key);
            int i = indexFor(hash, table.length);  // 根据hash值和表长计算出数组索引
            for (Entry<K,V> e = table[i]; e != null; e = e.next) {  // 遍历对应索引处的链表
                Object k;
                if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { // 键已存在
                    V oldValue = e.value;
                    e.value = value;       // 覆盖旧值
                    e.recordAccess(this);
                    return oldValue;       // 返回旧值
                }
            }
    
            modCount++;
            addEntry(hash, key, value, i);  // 键不存在,添加新的Entry
            return null;
        }
        private void inflateTable(int toSize) {
        // 转化为大于等于toSize的最小2次方
            int capacity = roundUpToPowerOf2(toSize); 
            threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
            table = new Entry[capacity];      // 创建table数组
            initHashSeedAsNeeded(capacity);
        }
      // 转化为大于等于toSize的最小2次方
        private static int roundUpToPowerOf2(int number) {
            return number >= MAXIMUM_CAPACITY
                    ? MAXIMUM_CAPACITY
                    : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
        }
        public static int highestOneBit(int i) {
            i |= (i >>  1);
            i |= (i >>  2);
            i |= (i >>  4);
            i |= (i >>  8);
            i |= (i >> 16);
            return i - (i >>> 1);
        }
      // 计算hash值
        final int hash(Object k) {
            int h = hashSeed;
            if (0 != h && k instanceof String) {
                return sun.misc.Hashing.stringHash32((String) k);
            }
    
            h ^= k.hashCode();
    
            // This function ensures that hashCodes that differ only by
            // constant multiples at each bit position have a bounded
            // number of collisions (approximately 8 at default load factor).
            h ^= (h >>> 20) ^ (h >>> 12);
            return h ^ (h >>> 7) ^ (h >>> 4);
        }
        void addEntry(int hash, K key, V value, int bucketIndex) {    // 装入指定桶
         // 元素个数大于阈值(表长*0.75)并且对应桶不为空
            if ((size >= threshold) && (null != table[bucketIndex])) {
                resize(2 * table.length);                    // 先扩容成原来的两倍
                hash = (null != key) ? hash(key) : 0;
                bucketIndex = indexFor(hash, table.length);
            }
    
            createEntry(hash, key, value, bucketIndex);    // 再插入
        }
        void resize(int newCapacity) {           // 扩容(需传入指定容量)
            Entry[] oldTable = table;           // 保存旧数组
            int oldCapacity = oldTable.length;  // 保存旧数组长度
            if (oldCapacity == MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return;
            }
    
            Entry[] newTable = new Entry[newCapacity];   // 创建新数组
            transfer(newTable, initHashSeedAsNeeded(newCapacity)); // 从旧转移到新的
            table = newTable;                         // table指向新的数组
          // 修改阈值
            threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
        }
      // 从旧数组转移至新数组
        void transfer(Entry[] newTable, boolean rehash) {
            int newCapacity = newTable.length;   // 新容量
            for (Entry<K,V> e : table) {           // 从旧数组中取Entry
                while(null != e) {               // 不为空      
                    Entry<K,V> next = e.next;
                    if (rehash) {
                        e.hash = null == e.key ? 0 : hash(e.key);
                    }
                    int i = indexFor(e.hash, newCapacity);  // 计算在新数组中的索引
                    e.next = newTable[i];
                    newTable[i] = e;
                    e = next;
                }
            }
        }

    图示:

    可见,因为是头插法,扩容后链表中的元素顺序变成了原来的逆序。多线程环境下易产生循环链表。

    常见疑问

    为什么初始容量必须是2的次方?

    构造方法中若指定初始容量,且不是2的次方,会在put方法中将其转化为大于等于它的最小2次方。

    keyhash值和length取余即可使元素对应的数组索引落在0~length-1范围内,可是JDK中并没有使用取余运算符,若使用取余运算符,在扩容时元素移动需要大量的取余计算效率较低,而与运算符效率高于取余运算符,因此jdk中采用了与运算符。

    索引计算

    JDK1.7中:

    static int indexFor(int h, int length) {  // 根据hash值和表长度计算对应的数组索引

        return h & (length-1);

    }

    JDK1.8中:

    tab[i = (n - 1) & hash]     // putVal方法中代码

    为了使与运算符达到取余运算符一样的效果,初始容量必须是2的次方。比如,当为初始容量为16时,length-1= 0000 0000 0000 0000 0000 0000 0000 1111,则任何一个值和(length-1)的与运算结果必然在0~15范围内。

    初始容量是2的次方,hash%length等于hash&(length-1)

    初始容量若不是2的次方,则hash%length 就不等于hash&(length-1)

    为什么默认负载因子是0.75

    默认负载因子(0.75)在时间和空间成本之间提供了一个很好的折衷方案。

    As a general rule, the default load factor (.75) offers a good tradeoff between time and space costs.

    为什么JDK1.8中转化为红黑树的阈值是8

    符合泊松分布,当一个桶中元素达到8个时,再有新元素添加进来的几率接近于0,也就是产生红黑树的几率并不大,红黑树的自平衡消耗比较大。而且TreeNode占用空间是Node的两倍。所以在小于等于8的时候,使用链表存储效率高(查询的时间复杂度是O(n)),大于8用红黑树效率高(查询的时间复杂度是O(logn))。

    源码注释:

    /*
    
         * Because TreeNodes are about twice the size of regular nodes, we
    
         * use them only when bins contain enough nodes to warrant use
    
         * (see TREEIFY_THRESHOLD). And when they become too small (due to
    
         * removal or resizing) they are converted back to plain bins.  In
    
         * usages with well-distributed user hashCodes, tree bins are
    
         * rarely used.  Ideally, under random hashCodes, the frequency of
    
         * nodes in bins follows a Poisson distribution
    
         * (http://en.wikipedia.org/wiki/Poisson_distribution) with a
    
         * parameter of about 0.5 on average for the default resizing
    
         * threshold of 0.75, although with a large variance because of
    
         * resizing granularity. Ignoring variance, the expected
    
         * occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
    
         * factorial(k)). The first values are:
    
         *
    
         * 0:    0.60653066
    
         * 1:    0.30326533
    
         * 2:    0.07581633
    
         * 3:    0.01263606
    
         * 4:    0.00157952
    
         * 5:    0.00015795
    
         * 6:    0.00001316
    
         * 7:    0.00000094
    
         * 8:    0.00000006
    
         * more: less than 1 in ten million
    
         */
  • 相关阅读:
    Oracle系列二 基本的SQL SELECT语句
    Oracle系列一 SQL语句基本概念和学习准备
    Android 动态更换桌面图标
    Linux_CentOS下搭建Nodejs 生产环境-以及nodejs进程管理器pm2的使用
    Linux_CentOS中Mongodb4.x 安装调试、远程管理、配置 mongodb 管理员密码
    Linux_CentOS 中systemctl 管理服务、防火墙 firewalld 以及 SELinux 配置
    Linux_CentOS 内存、cpu、进程、端口、硬盘管理
    Linux_CentOS中的MySQL 数据库的安装调试、远程管理
    LInux_CentosOS中yum安装jdk及配置环境变量
    Linux_CentOS软件安装调试 源代码包编译安装和 二进制包配置
  • 原文地址:https://www.cnblogs.com/jiazhongxin/p/12895170.html
Copyright © 2020-2023  润新知