HashMap 1.8 源码个人解读

HashMap 1.8 源码个人解读

该篇文章为个人阅读源码后的理解,有错误还请指点。本文的图源自链接 5 。

参考文章

  1. 掘金网 -> HashMa 底层逻辑原理
  2. 掘金网 -> 最详细的 hash() 算法解析
  3. GitHup -> 三太子敖丙的 HashMap 面试问答
  4. CSDN -> HashMap实现原理和源码分析
  5. 知乎 -> Java 8系列之重新认识HashMap
  6. 博客网 -> HashMap实现原理及源码分析
HashMap 简介

  HashMap<K,V> 继承自 AbstractMap 还实现了 Map<K,V>,Cloneable,Serializable 的接口。内部存在一个静态类 Node 与 TreeNode 是需要知道的,Node 是存储数组上链表的单个节点对象(JDK1.7 中为 Entry),TreeNode 是 JDK1.8 新加入的 红黑树存储结构(本编文章不对红黑树介绍)。

  HashMap 是基于拉链法(链地址法)实现的一个散列表,内部由 数组 + 链表 + 红黑树 实现。hashmap 在存储为 null 的主键时会固定的存放在数组下标 0 的位置上,这点上注意在线程安全的 ConcurrentHashMap 是不能存储 null 主键的。

26341ef9fe5caf66ba0b7c40bba264a5_r
HashMap 数据与存储结构

  从下图中可见 数组、链表、红黑树 3个关键字,其中链表长度大于 8 (TREEIFY_THRESHOLD)时转换为红黑树其实只是尝试进行转换,具体会不会真实转换还得看 table 的长度是否达到了 64 (MIN_TREEIFY_CAPACITY)位,而且如果当红黑树的节点缩小至 6 (UNTREEIFY_THRESHOLD)时还会从红黑树化解为链表的结构,这些都是 HashMap 作者测试出比较平衡的几个阈值大小。

hashmap结构图

  接着说说 HashMap 的存储步骤,首先在 put 时要取得数组 table 的下标,大致可以分为这 3 步:取 key 的 hashcode -> 高位运算 -> 取模计算。那么问题来了,很多人只知道 h & (table.length -1) ,但是却不明白高位运算是什么意思,这个其实也可以称为 “扰乱算法”,这个将在后面的 hash() 方法解析中说明。

hashmap获取下标图解
hash 算法介绍

  Hash 就是把任意程度的输入,通过 散列算法 变换成固定长度的数据,该输出值俗称 “散列值”,所以 Hash 表也称为 散列表。但是输入值不同,不代表转换后的 “散列值” 不同,不同的输入值,是有概率会产生相同的散列值的,这种现象可以称作 “碰撞”(hash 冲突也是这么来的),不过不同散列值它们的输入值肯定不同。

具体的 散列算法,可以在头部 相关链接2 中进行详细了解。

  在 HashMap 中的主要作用就是根据对象的 hashcode 进行高位运算的 2 次加工,但是要注意的是,如果重写了 equals() 方法,就必须将 HashCode() 方法也进行与 equals() 相同比较逻辑重写。因为在 HashMap 调用 get() 方法时,先是按照 h & (length - 1) 来进行寻址的,也就是 [哈希码 & ( hashmap长度 - 1 )] 方式获取的下标,当找到元素位置后的链表遍历则是 通过的 equals() 方法进行的比较,可想而知,如果你 hashCod 的得出的整数,方法没有进行重写,而是按照 Object 基类的内存地址转换来寻找,然后 equals 链表比较时又按照了你重写的逻辑寻找,将不是原来的 Object 基类的 “==” 内存地址比较逻辑,(object 的 equals 是比较的地址) 那么结果肯定将会出现与你期望的效果不一致。

  在 HashMap 的 hash 算法中返回值会再次将对象的 hashcod 进行 扰动计算,降低 hash 冲突的概率。在 jdk1.8 中扰动计算有所改动,由 jdk1.7 的 4 次 右移异或混合 改为 16 次

具体的 扰动计算,可以在头部 相关链接2 中进行详细了解。

public class test {
    public static void main(String[] args){
        TTT t1 = new TTT("张三",1);
        TTT t2 = new TTT("张三",2);

        Map map = new HashMap();
        map.put(t1,t1);

        // 我这里按照业务想根据 name 去寻找,本以为只要重写 equals 就行了,
        // 但是没有重写 hashCode,他们两的名字明明都是相等的,但是就是找不到
        // 因为 Object 的 hashCode 是按照对象本身的内存地址转换的 哈希码
        // 然后根据 哈希码 通过 indexFor 来寻址找到的下标
        System.out.println(map.get(t2));

    }
}

class TTT{
    String name;
    Integer id;

    public TTT(String name, Integer id) {
        this.name = name;
        this.id = id;
    }

    @Override
    public boolean equals(Object obj) {
        TTT t1 = (TTT) obj;
        System.out.println("调用了 equals 方法" + obj);
        if(this.name.equals(t1.name)){
            return true;
        }
        return false;
    }

    @Override
    public String toString() {
        return "ttt{" +
                "name='" + name + '\'' +
                ", id=" + id +
                '}';
    }

//    @Override
//    public int hashCode() {
//        return this.name.hashCode();
//    }
}

Node 链表的存储对象

  Node 类是实现的 Map.Entry 接口,在 Node 类中,有 4 个成员属性分别是 hash,key,value,next ,方法为 Node(),getKey(),getValue(),toString(),hashCode(),setValue(),equals(),在这些方法中,hashCode() 计算是取 key 的 hashcode 和 value 的hashcode 进行 ^ 异操作,并调用了 Objects 的 hashCode 的非空判断方法。

  在 setValue() 方法中,除了替换之前的 value 值,还会返回老的 value 值。在 equals() 方法中会判断传进来的值是否为 Map.Entry 的类型,这个类型是接口套接口的用法,很有意思,然后才会比较调用自身的 key 和 value 的 equals() 方法与传进来的进行比较。

额外补充

  hashmap 的 TreeNode 红黑树是属于 Node 的 孙类,Node 的子类为 LinkedHashMap.Entry ,然后由 TreeNode 继续继承了 LinkedHashMap.Entry。

// 1.8 
static class Node<K,V> implements Map.Entry<K,V> {
    // 存储高位移动计算后的 hash 值
    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;
    }

    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;
    }
}

成员属性与静态常量

  另外在 HashMap 源码中,还存在着很多的 static final 静态常量作为阈值的存在

例如:

  1. static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认大小 16
  2. static final float DEFAULT_LOAD_FACTOR = 0.75f; // 负载因子
  3. static final int TREEIFY_THRESHOLD = 8; // 链表转红黑树的长度阈值
  4. static final int MAXIMUM_CAPACITY = 1 << 30; // hashmap 的最大扩容
  5. … … 其他了解的常量 ↓
  6. static final int MIN_TREEIFY_CAPACITY = 64; // 用来判断是否真的需要转红黑树的阈值
  7. static final int UNTREEIFY_THRESHOLD = 6; // 红黑树转链表的阈值

重要成员属性:

  1. transient Node<K,V>[] table; // 存储所有节点的数组,也可以叫做 哈希桶数组
  2. transient Set<Map.Entry<K,V>> entrySet; // 缓存相关的,没有了解
  3. transient int size; // 实际存储的元素数量(实际存储于数组长度分开理解)
  4. transient int modCount; // 记录内部结构发生变化的次数,put操作(覆盖值不计算 fail-fast机制)
  5. int threshold; // 允许的最大的存储的元素数量,通过 table.length*loadFactor 增长因子得出
  6. final float loadFactor; // 数据的增长因子(负载因子),默认为0.75。在进行扩容操作会使用到。
fail-fast 机制
  1. CSDN -> Java提高篇(三四)-----fail-fast机制
  2. OSCHINA -> hashmap 的 modCount

方法解析
static - hash()

  这个方法在 jdk1.7 的时候会参差两三行的 扰乱算法,但是在 jdk1.8 后 改良 了一些,因为加入了 红黑树 就不必要在像以前那样节省作用不大的高低位位移的 cpu 资源了,用来将资源提供给红黑树计算,性能比会更高。

  高低位位移是为了二进制后的数 高位影响低位,移位到后面 与 table.length - 1 的值去&计算数组的准确下标位置,使一些比较规则的 hashcode 值(包括开发者重写后后的 hashcode() 的方法)更具有 散列性,能更 均匀的分布

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

科普

  实际上java中%符号是求余(rem),而不是取模(mod)。在除数和被除数都为正数时,取模和取余的结果是一样的,区别在于有负数出现的时候。所以我们容易混淆这两个概念,而在某些编程语言中%符号代表取模,所以很难区分它的叫法,我们知道%在自己使用的编程语言中代表什么就行了。

123 / 8

// 用移位运算可以表示为右移3位:

1111011 >> 3

余数正是被移位运算移走的最低3位,011,也就是余数为3。

原来 移位运算移走的那些二进制就是余数N % M == N & (M - 1)


static - tableSizeFor()

  这是将目标参数转换为 最接近 => 的 2 次方数 的方法,主要用途就是用于将一些不确定的值转换可控范围的值,可以用来 降低 hash 冲突 的情况,最容易理解的就是在 hash 带参构造中将使用者传入的 容器大小 转为 => 2 次方数的值,原理就是二进制的 补位 操作,Integer.highestOneBit 方法,也是运用的这个原理。

// 1.8
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;
}

new - HashMap()

  以下图为 帮助文档 中展示的 hashmap 所有构造方法,在用户传入 initialCapacity < 0 或 loadFactor <= 0时将会抛出异常,并且 initialCapacity 的初始容量大小也不能超过阈值 1<<<30(1073741824)的大小,否则只会按照最大值(MAXIMUM_CAPACITY) 设置,这个值是符合 2 的幂次方规则,超过这个值后的 hashmap 将不会扩容,只会继续在链表或红黑树上继续添加元素。

  另外 hashmap 的作者设计时为了考虑恒定性能,选择的 16 与 0.75 也是经过计算测试过的,提供为了空间与时间的良好的折中,初始容量和负载因子是比较影响性能的两个重要参数

补充:

  因为 integer 的 MAX_VALUE 最大也不过是 2147483647 ,也就是 2 的 32 次方 -1 的大小,所以 1 << 30 其实也就是 2 的 29 次方,如果再次按照扩容机制的 2 倍扩容将会 = 2147483648,也就是超出了 integer 的最大值大小。

image-20200727110826953

// 1.8 的有参构造
public HashMap(int initialCapacity, float loadFactor) {
    // 容量大小不能为 0,如果大小大于了最大阈值 1<<30 的话就 = MAXIMUM_CAPACITY
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    // 自定义的负载因子浮点型只能为 > 0 的数
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    
    // 这里注意调用了这个方法,是将容量调整到 往上取最接近 2 次方的值,例如 7 -> 8, 10 -> 16
    // 在第一次 put 操作,扩容数组时,会将这个 threshold 作为数组有参构造的出事容量,
    // 然后重新计算这个值 按照 table.length *  loadFactor 
    this.threshold = tableSizeFor(initialCapacity);
}

public HashMap(int initialCapacity) {
    // 默认 0.75 负载因子调用 上面的带参构造
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// 1.8 的无参构造
public HashMap() {
    // 可以看出在无参构造中并不会初始化很多的属性,只是设置了 0.75 的负载因子
    // 而其他 int 都是默认 0 数组则是 null,在后面扩容时分支会不同
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

object - get()

  获取元素的方法,主要调用了 getNode() 方法的逻辑,并返回出去。

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

object - getNode()

  获取元素的封装起来的具体逻辑代码。

// 1.8 手动格式化后的容易理解的样貌
final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab = table; 
    int n = tab.length;
    Node<K,V> first = tab[(n - 1) & hash];
    
    Node<K,V> e = first.next; 
    K k = first.key;
    
    // 如果数组不为 null,并且长度 > 0,
    // 并且通过 hash() 方法转换后的 key 值与 长度 - 1 方式相 & ,
    // 得出准确下标(与 1.7 indexFor() 方法相似)判断不为空
    if (tab != null 
        && n > 0 
        && first != null) {
        
        // 如果头元素的 hash 与 传进来的 hash 相同,并且 key 也是相同的(== 和 equals 满足1个)
        if (first.hash == hash && // always check first node
            (k == key || (key != null && key.equals(k))))
            // 说明第一个元素就是直接返回出去
            return first;
        
        // 如果 e 也就是下一个元素不为 null 则进入
        if (e != null) {
            // 判断是否为 红黑树结构,成立则用红黑树方式继续查找并返回出去结果
            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;
}

object - containsKey()

  判断某个键是否存在,与 get 相同,也是调用的 getNode() 方法。

public boolean containsKey(Object key) {
    return getNode(hash(key), key) != null;
}

object - put()

  将 key 根据 干扰计算 后,并传入核心逻辑代码进行添加。

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

object - putVal()

 图解 1.8 的 put 具体流程

hashmap的put流程

  源码解析

// 1.8 手动格式化后的容易理解的样貌
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab = table; 
    Node<K,V> p = tab[i = (n - 1) & hash]; 
    int n = tab.length;
    int i; // 这个 i 没被使用过,可能作者忘记使用了
    
    // 如果数组还没有初始化使用过,则进行一次初始化,hashmap 在构建时是不会初始化的,除非使用时才初始化数组
    if (tab == null || n == 0)
        n = (tab = resize()).length;
    // 如果根据计算好的下标找到的节点依然是 null 的,则直接赋值添加
    if (p == null)
        tab[i] = newNode(hash, key, value, null);
    // 进入这里代表需要遍历链表或者红黑树了
    else {
        Node<K,V> e; 
        K k = p.key;
        // 判断第一个元素是不是重复的元素,是的话则直接赋值给 e 不用遍历
        if (p.hash == hash &&
            (k == 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) {
                // 循环里的 e 不断迭代 .next
                // 如果迭代为 null 则直接创建一个新节点,添加在尾元素中,尾插方式,1.7 为头插
                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;
                }
                // 不断判断是否存在于链表身体中,存在则直接结束循环,break 掉,此时的 e 则是目标元素
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                // 迭代更新
                p = e;
            }
        }
        
        // 最后判断 e 是否为 null,存在则代表着是被 覆盖掉,这里会将老的值返回给使用者
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            // 这里在 put 调用时固定了 onlyIfAbsent = false,始终成立,
            // 就是将传入新的 value 覆盖掉老的 value 
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            // 方法体为空,在 LinkedHashMap 中有被重写
            afterNodeAccess(e);
            // 返回老的 value
            return oldValue;
        }
    }
    // 如果不是覆盖添加,是添加了一个新的元素,则添加次数 ++,这是为了多线程并发做的准备
    ++modCount;
    // 如果添加后的实际元素大小 > 了最大的阈值则扩容
    if (++size > threshold)
        resize();
    // 方法体为空,在 LinkedHashMap 中有被重写
    afterNodeInsertion(evict);
    return null;
}

object - resize()

  在 1.7 中扩容后会有重新 rehash 的操作,但是很难执行那个 if ,一般都是与 1.8 一样的扩容原理,所以在 1.8 中直接废除了 rehash 的判断,在 1.7 中的扩容机制会使得链表出现 倒置的效果!!!1. 7 总所周知的知道 头插 会使得程序在多并发容易出现环形链表的情况,所以在构造时如果知道自己使用的大小,可以直接固定大小和负载因子,从而避免扩容的操作,另外负载因子设置为 1 可以节省内存,但是会有减低性能的效果。

  只有在下标节点只有一个首元素时才会重新 hash 计算一个新坐标位置,否则就按照容量的 2次方 bit 头判断相与 = 0 和 else 的情况,符合 if 条件的将会放在原地,进入 else 的会添加在 [下标 + 老数组长度] 的位置上,这也是 1.8 的改良不错的地方。

  为了方便理解上面的 hash 扩容机制,以下图举个例子,初始容量为 16 ,key 1 和 key 2 是存储在 Node 节点上计算好的 hash 码值,在 a 中,可以看出 key1 和 key2 相与运算后都是在 101 在下标 7 上的。在扩容为 32 容量后的 b 中,他们其实只需要看 n -1 的首位元素 1 和 key 的hash 相与运算,能否 = 0 就好了,具体结果再看 第二张图。

扩容&分组图解

  可以看出相与运算后 != 0 的情况刚好是 原位置下标 + 老数组长度 得到的总和 转换为 2 进制的数字。

扩容&分组图解2

  源码分析

final Node<K,V>[] resize() {
    // 扩容前将目前的信息作为 老old 变量备份一份
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;// 长度
    int oldThr = threshold; // 最大可允许的存储元素 无参构造时默认值为0
    int newCap = oldCap << 1// 扩容后的长度,等同于 老长度 * 2,用 << 效率更高
    int newThr = 0;
    
    // 如果老的长度 > 0 则代表不是第一次扩容,是 put 时进行的扩容,table != null,
    if (oldCap > 0) {
        // 判断长度是否已经达到了最大扩容阈值 1<<<30,如果达到了则将 threshold 也设置为最大容量,
        // 这里的最大容量使用了 Integer.MAX_VALUE 就代表着所有空间都可以使用,
        // 而不需要 .length * 负载因子的方式限制阈值了,结束后并返回了老的 table ,因为没有改变
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 或者如果扩容后的长度符合最大扩容的阈值范围,并且 老长度 >= (1<<4) 就将阈值也进行相对应的位移 1 位
        // 也就等同于 newCap * threshold ,使用 << 方式更效率
        else if (newCap < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    // 进入这里代表着 table == null,代表着有参构造出的 hashmap,在有残构造中 oldThr = 的 threshold 
    // 无论如何都是为 >0 的,是通过 tableSizeFor() 方法得出的,即是负数或0 都会最终成为 1
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    // 无参构造时因为没有给可用元素 threshold 赋值,所以默认为 0 ,而且 table == null,所以进入这里
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;// 16
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);// 16 * 0.75
    }
    // ... 可能是上面newThr = oldThr << 1时,最高位被移除了,变为0
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    // 将新的可用容量复制给成员属性 threshold 上
    threshold = newThr;
    // 根据得出的新数组长度创建一个新 Node 数组
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    // 如果 老的数组不是 null 了,即代表初始化后了
    if (oldTab != null) {
        // 开始遍历 Node 数组
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e = oldTab[j];// 获取元素
            if (e != null) {
                oldTab[j] = null;
                // e.next == null 则代表只有第一个元素
                if (e.next == null)
                    // 直接进行重新的下标运算然后设置上去,这里的 Node e 的 hash 是之前干扰算法计算存储过的
                    newTab[e.hash & (newCap - 1)] = e;
                
                // 判断是否为 红黑树 结构
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    // 低位的头和尾节点
                    Node<K,V> loHead = null, loTail = null;
                    // 高位的头和尾节点
                    Node<K,V> hiHead = null, hiTail = null;
                    // 用于迭代的指针
                    Node<K,V> next;
                    do {
                        // 开始迭代
                        next = e.next;
                        // 直接以老数组长度 2 的次方数(不是 - 1) 与 hash 码与运算,此时只会出现两种情况
                        // 因为 2 的次方数尾巴都是 0 结尾,只要判断头的 1 与 hash 码对应的数比较即可
                        // 是0的话索引没变,是 1 的话索引变成 “原索引+oldCap”
                        if ((e.hash & oldCap) == 0) {
                            // loHead 与 loTail 如同两个指针,一个指向链表的头一个指向尾
                            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);
                    // 循环结束后会分出两条链表结构,一个是 = 0 的能够继续查找到的,
                    // 另一个是 != 0 的可以在扩容后的数组长度找到的,一会将有图解
                    if (loTail != null) {
                        loTail.next = null;
                        // 继续放入原始位置
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        // 放入 [ 下标 + 老长度 ] 的位置
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    // 返回新的表格
    return newTab;
}

object - treeifyBin()
// 1.8 
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    // 如果数组容量太小 <64 则不进行转红黑树,会继续选择扩容 resize();
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)// MIN_TREEIFY_CAPACITY = 64
        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);
    }
}
总结:1.7 与 1.8 的 hashmap 区别
  1. 红黑树 -> TreeNode 的加入 (数组 + 链表)变为(数组 + 链表 + 红黑树)复杂度从O(n)变成O(logN)提高了效率)
  2. Node -> 与 JDK 1.7 的对比(Entry类),仅仅只是换了名字
  3. 静态常量 -> 加入了 红黑树相关的阈值静态常量,Node 长度 >= 8-1 尝试转红黑树,table 大小 > 64 转换红黑树,Node 长度 <= 6 由树变链表
  4. 扰动处理 -> 高位运算更精简, jdk1.7 的 4 次 右移异或混合 改为 16 次
  5. put 存储 -> 新增后进行长度判断尝试转红黑树的分支
  6. resize 扩容 -> 头插变尾插,去除 rehash 判断,并使用了 新增参与运算的位 ==0 和 else 的方式进行转移重新存储在 [下标 + 老数组长度] 的位置

  列出两张 1.7 与 1.8 的扩容图解,可以简单的看出一个在扩容后变成了倒序,而 1.8 则不是这样,倒序是因为使用的头插方式,并发情况变为环形链表也有这个因素,在 1.8 变为尾插 改良了很多。

1.7 在扩容后的图解

1.7中头插put图解

1.8 在扩容后的图解

分组&扩容图解3
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值