深度解读jdk8 HashMap设计与源码

HashMap是基于哈希表Map接口实现,以key-value存储形式存在,即主要用来存放键值对。其中,key-value均可为null,映射也不是有序的。HashMap的实现不是同步的,也即线程不安全。

一、存储特点

1.1 数据结构的演变

jdk1.7- :由数组 + 链表组成,数组是主体,链表主要是为了解决哈希冲突(即通过key计算的数组索引值相同)而存在的("拉链法"解决冲突)。

jdk1.8+ :由数组 + 链表+红黑树组成,在解决哈希冲突时有了较大的变化,当链表长度大于阈值 (红黑树的边界值,默认为8),并且当前数组长度大于64 时,此索引位置上数据存储结构转变为红黑树

补充:链表转换成红黑树之前,即使阈值大于8,但数组长度小于64时,并不会将链表变为红黑树,而是对数组进行扩容。 目的是:当数组较小情况下,尽量避开红黑树结构。因为数组较小情况下,变为红黑树结构,反而会降低效率(红黑树需要逬行左旋,右旋,变色这些操作来保持平衡)。同时数组长度小于64时,搜索时间相对要快些。所以结上所述为了提高性能,底层阈值大于8并且数组长度大于64时,链表才转换为红黑树,具体可以参考 treeifyBin() 方法。

当然虽然增了红黑树作为底层数据结构,结构变得复杂了,但是阈值大于8并且数组长度大于64时,链表转换为红黑树时,效率也变的更高效。

1.2 特点

  • 存储无序。
  • 键和值均可为null,但是键位置只能存在一个null。
  • 键位置是唯一的,是底层的数据结构控制的。
  • jdk1.8前数据结构是链表+数组 ,jdk1.8之后是链表+数组+红黑树
  • 阈值(边界值)> 8 并且数组长度大于 64,才将链表转换为红黑树,变为红黑树的目的是为了提超查询。

二、类结构

2.1 继承关系

说明

  • Cloneable空接口,表示可以克隆。创建并返回 HashMap 对象的一个副本。
  • Serializable序列化接口。标记HashMap对象可以被序列化和反序列化。
  • AbstractMap提供了Map实现接口。以最大限度地减少实现此接口所需的工作。

问题:通过上述继承关系我们发现一个很奇怪的现象,就是HashMap已经继承了AbstractMap,而AbstractMap类实现了Map接口,那为什么 HashMap还要在实现Map接口呢?同样在ArrayList中LinkedLis中都是这种结构。

答案:据Java集合框架的创始人Josh Bloch描述,这样的写法是一个失误。在Java集合框架中,类似这样的写法很多,最幵始写Java集合框架的时候,他认为这样写,在某些地方可能是有价值的,直到他意识到错了。很显然jdk的维护者,后来不认为这个小小的失误值得去修改,所以就这样保留下来了。

2.2 成员变量

java 复制代码
public class HashMap<K, V> extends AbstractMap<K, V>
        implements Map<K, V>, Cloneable, Serializable {
    /** 序列化版本号 */
    private static final long serialVersionUID = 362498820763181265L;

    /** 集合初始化容量,必须是2的n次幂,默认的初始容量是16 */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

    /** 集合最大容量,默认为2的30次幂 */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /** 默认加载因子,默认值 0.75 */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /** 当链表元素数量超过该值,则会转为红黑树(jdk1.8新增) */
    static final int TREEIFY_THRESHOLD = 8;

    /** 当链表的值小于6,则会从红黑树转回链表 */
    static final int UNTREEIFY_THRESHOLD = 6;

    /** 当存放元素数量超过该值,表中的桶才能转换为红黑树,否则桶内元素超过指定条件时只会进行扩容 */
    static final int MIN_TREEIFY_CAPACITY = 64;

    /** 数组结构 */
    transient Node<K,V>[] table;

    /**存放缓存数据 */
    transient Set<Entry<K,V>> entrySet;

    /**存放元素数量 */
    transient int size;

    /** 用来记录HashMap修改次数,即每次扩容和更改map结构的计数器 */
    transient int modCount;

    /** 扩容临界值,当存放元素数量超过临界值(容量*负载因子)时,会进行扩容 */
    int threshold;

    /** 哈希表加载因子 */
    final float loadFactor;
}

2.3 构造方法

2.3.1 HashMap(initialCapacity, loadFactor)

java 复制代码
/**
 *  指定容量大小和负载因子的构造函数
 *  @param initialCapacity 容量
 *  @param loadFactor 负载因子
 */
public HashMap(int initialCapacity, float loadFactor) {
   	   // 判断初始化容量initialCapacity是否小于0
       if (initialCapacity < 0)
           // 如果小于0,则抛出非法的参数异常IllegalArgumentException
           throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
   	   // 判断初始化容量initialCapacity是否大于集合的最大容量MAXIMUM_CAPACITY
       if (initialCapacity > MAXIMUM_CAPACITY)
           // 如果超过MAXIMUM_CAPACITY,会将MAXIMUM_CAPACITY赋值给initialCapacity
           initialCapacity = MAXIMUM_CAPACITY;
   	   // 判断负载因子loadFactor是否小于等于0或者是否是一个非数值
       if (loadFactor <= 0 || Float.isNaN(loadFactor))
           // 如果满足上述其中之一,则抛出非法的参数异常IllegalArgumentException
           throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
       // 将指定的负载因子赋值给HashMap成员变量的负载因子loadFactor
       this.loadFactor = loadFactor;
       this.threshold = tableSizeFor(initialCapacity);
}
/*
 * 返回比指定初始化容量大的最小的2的n次幂
 * @param 容量
*/
static final int tableSizeFor(int cap) {
    // 防止cap已经是2的幂。如果cap已经是2的幂,又没有这个减1操作,
    // 则执行完后面的几条无符号操作之后,返回的 apacity将是这个cap的2倍。
    int n = cap - 1;  
    n |= n >>> 1;  // 最高位非零1位右移1位,逻辑或n,此时可得最高2位为11
    n |= n >>> 2;  // 最高位非零2位右移2位,逻辑或n,此时可得最高2位为1111
    n |= n >>> 4;  ...
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

注意:在tableSizeFor方法体内部将计算后的数据返回给调用这里了,并且直接赋值给threshold边界值了。有些人会觉得这里是一个bug,应该这样书写:this.threshold = tableSizeFor(initialCapacity) * this.loadFactor,这样才符合threshold的语意(当HashMap的size到达threshold这个阈值时会扩容)。但是请注意,在jdk8以后的构造方法中,并没有对table这个成员变量进行初始化,table的初始化被推迟到了put方法中,在put方法中会对threshold重新计算。

2.3.2 HashMap(Map m)

java 复制代码
// 构造一个映射关系与指定 Map 相同的新 HashMap。
public HashMap(Map<? extends K, ? extends V> m) {
     // 负载因子loadFactor变为默认的负载因子0.75
     this.loadFactor = DEFAULT_LOAD_FACTOR;
     putMapEntries(m, false);
}

final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    //获取参数集合的长度
    int s = m.size();
    if (s > 0) { // 判断参数集合是否包含元素
        if (table == null) { // 判断table是否已经初始化
              // 未初始化,s为m的实际元素个数
              float ft = ((float)s / loadFactor) + 1.0F;
              int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY);
              // 计算得到的t大于阈值,则重新初始化阈值
              if (t > threshold)
                  threshold = tableSizeFor(t);
        }
        // 已初始化,并且m元素个数大于阈值,进行扩容处理
        else if (s > threshold)
            resize();
        // 将m中的所有元素添加至HashMap中
        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);
        }
    }
}

问题float ft = ((float)s / loadFactor) + 1.0F这一行代码中为什么要加1.0Fs/loadFactor的结果是小数,加1.0F(int)ft相当于是对小数做一个向上取整以尽可能的保证更大容量,更大的容量能够减少 resize 的调用次数。所以+ 1.0F是为了获取更大的容量。

2.4 成员方法

2.4.1 添加节点元素

put方法是比较复杂的,实现步骤大致如下:

  1. 先通过 hash 值计算出 key 映射到哪个桶;
  2. 如果桶上没有碰撞冲突,则直接插入;
  3. 如果出现碰撞冲突了,则需要处理冲突:
    • 如果该桶使用红黑树处理冲突,则调用红黑树的方法插入数据;
    • 否则采用传统的链式方法插入。如果链的长度达到临界值,则把链转变为红黑树;
  4. 如果桶中存在重复的键,则为该键替换新值 value;
  5. 如果 size 大于阈值 threshold,则进行扩容;

具体的方法如下:

java 复制代码
public V put(K key, V value) {
	return putVal(hash(key), key, value, false, true);
}

static final int hash(Object key) {
	int h;
	/*
	1)如果key等于null:返回的是0.
	2)如果key不等于null:首先计算出key的hashCode赋值给h,然后与h无符号右移16位后的
		二进制进行按位异或得到最后的hash值
	*/
	return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

问题 :(h = key.hashCode()) ^ (h >>> 16),高16bit不变,低16bit和高16bit做了一个异或得到的hashCode转化为32 位二进制,为什么要这样操作呢?

答案:如果当n即数组长度很小,假设是16的话,那么n - 1即为 1111 ,这样的值和hashCode直接做按位与操作,实际上只使用了哈希值的后4位。如果当哈希值的高位变化很大,低位变化很小,这样就很容易造成哈希冲突了,所以这里把高低位都利用起来,从而解决了这个问题。

2.4.2 内部添加节点元素

java 复制代码
public V put(K key, V value) {
	return putVal(hash(key), key, value, false, true);
}

/**
 * @param hash key的hash值
 * @param key 原始key
 * @param value 要存放的值
 * @param onlyIfAbsent 如果true代表不更改现有的值
 * @param 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;
    // 判断集合是否新创建,即tab为null
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 计算结点位置是否已存在元素,存在则表示有希碰撞冲突情况,否则直接将元素插入该位置
    // 将已存在的桶数据保存到变量p中
    if ((p = tab[i = (n - 1) & hash]) == null)
        // 创建一个新的结点存入到桶中
        tab[i] = newNode(hash, key, value, null);
    else {  // 执行else说明tab[i]不等于null,表示这个位置已经有值了
        Node<K,V> e; K k;
        
        // 1. 元素hash值相等,但是不能确定是同一个值
        // 2. key是否是同一实例,或者值是否相等
        if (p.hash == hash && 
            ((k = p.key) == key || (key != null && key.equals(k))))
                // 说明:两个元素哈希值相等,并且key的值也相等,将旧的元素整体对象赋值给e,用e来记录
                e = p;
        // hash值不相等或者key不相等;判断p是否为红黑树结点
        else if (p instanceof TreeNode)
            // 放入树中
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else { // 说明是链表结点
            // 1)如果是链表的话需要遍历到最后结点然后插入
            // 2)采用循环遍历的方式,判断链表中是否有重复的key
            for (int binCount = 0; ; ++binCount) {
                // 判定p.next是否到达链表的尾部
                // 取出p.next赋值给e
                if ((e = p.next) == null) {
                	// 将节点插入到链表的尾部
                    p.next = newNode(hash, key, value, null);
                    // 判断是否达到转换为红黑树的临界条件,如果是则调用treeifyBin转换为红黑树
                    // 注意:treeifyBin方法会对数组长度进行检查,如果小于MIN_TREEIFY_CAPACITY会对数组进行扩容
                    if (binCount >= TREEIFY_THRESHOLD - 1) 
                        // 转换为红黑树
                        treeifyBin(tab, hash);
                    // 跳出循环
                    break;
                }
                
                // 没有到达链表尾部,判断key值是否相同
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    // 因为已将e保存,所以跳出循环
                    break;
                // 说明新添加的元素和当前结点不相等,继续查找下一个结点。
                // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
                p = e;
            }
        }
        // 表示在桶中找到key值、hash值与插入元素相等的结点
        // 也就是说通过上面的操作找到了重复的键,所以这里就是把该键的值变为新的值,并返回旧值
        // 这里完成了put方法的修改功能
        if (e != null) { 
            // 记录e的value
            V oldValue = e.value;
            // onlyIfAbsent为false或者旧值为null
            if (!onlyIfAbsent || oldValue == null)
                // 用新值替换旧值
                // e.value 表示旧值  value表示新值 
                e.value = value;
            // 访问后回调
            afterNodeAccess(e);
            // 返回旧值
            return oldValue;
        }
    }
    // 修改记录次数
    ++modCount;
    // 判断实际大小是否大于threshold阈值,如果超过则扩容
    if (++size > threshold)
        resize();
    // 插入后回调
    afterNodeInsertion(evict);
    return null;
}

2.4.3 将链表转换为红黑树

结点添加完成之后判断此时结点个数是否大于 TREEIFY_THRESHOLD 临界值 8,如果大于则将链表转换为红黑树,方法为treeifyBin。

java 复制代码
/**
 * @param tab 数组
 * @param hash 哈希值
 */
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    // 如果当前数组为空,或者数组的长度小于进行树形化的阈值(MIN_TREEIFY_CAPACITY = 64),就去扩容。
    // 而不是将结点变为红黑树。
    // 目的:如果数组很小,那么转换红黑树,然后遍历效率要低一些。
    //       这时进行扩容,那么重新计算哈希值,链表长度有可能就变短了,数据会放到数组中,这样相对来说效率高一些。
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        //扩容方法
        resize();
    // 获取key哈希值的桶元素,判断是否为空
    else if ((e = tab[index = (n - 1) & hash]) != null) {
   	    // 执行步骤
   	    // 1. 先将元素转换为红黑树节点
       	// 2. 然后再将其设置为链表结构
       	// 3. 最后将链表结构转换为红黑树
        	
        // hd:红黑树的头结点   tl:红黑树的尾结点
        TreeNode<K,V> hd = null, tl = null;
        do {
            // 将当前桶结点替换为红黑树节点
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null) // 首次操作,,红黑树尾节点为空
                hd = p; // 将新创键的p结点赋值给红黑树的头结点
            else {
                p.prev = tl; // 将上一个结点p赋值给现在的p的前一个结点
                tl.next = p; // 将现在结点p作为树的尾结点的下一个结点
            }
            tl = p;
          // 直到循环到链表末尾,以及将链表元素都替换为红黑树节点
        } while ((e = e.next) != null);
        // 让桶中的第一个元素即数组中的元素指向新建的红黑树的结点,
        // 以后这个桶里的元素就是红黑树,而不是链表数据结构了
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

2.4.4 扩容

什么时候需要扩容 :当 HashMap 中的元素个数 超过数组大小(数组长度)*loadFactor(负载因子)时,就会进行数组扩容,loadFactor 的默认值是 0.75。

进行扩容,会伴随着一次重新 hash 分配,并且会遍历 hash 表中所有的元素,是非常耗时的。在编写程序中,要尽量避免 resize。

HashMap 在进行扩容时,使用的 rehash 方式非常巧妙,因为每次扩容都是翻倍,与原来计算的 (n - 1) & hash 的结果相比,只是多了一个bit位,所以结点要么就在原来的位置,要么就被分配到 "原位置 + 旧容量" 这个位置。

因此,在HashMap扩容时,不需要重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就可以了,是0的话索引没变,是1的话索引变成 "原位置 + 旧容量" 。 正是因为这样巧妙的 rehash 方式,既省去了重新计算 hash 值的时间,而且同时,由于新增的 1bit 是 0 还是 1 可以认为是随机的,在 resize 的过程中保证了 rehash 之后每个桶上的结点数一定小于等于原来桶上的结点数,保证了 rehash 之后不会出现更严重的 hash 冲突,均匀的把之前的冲突的结点分散到新的桶中了。

源码 resize 方法的解读

java 复制代码
final Node<K,V>[] resize() {
    // 得到当前数组
    Node<K,V>[] oldTab = table;
    // 如果当前数组等于null长度返回0, 否则返回当前数组的长度
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    // 当前阀值,默认是12(16*0.75)
    int oldThr = threshold;
    int newCap, newThr = 0;
    // 如果旧数组长度大于0, 开始计算扩容后的大小
    // 小于等于0,没有扩容的必要
    if (oldCap > 0) {
        // 超过最大值就不再扩充了,就只好随你碰撞去吧
        if (oldCap >= MAXIMUM_CAPACITY) {
            // 修改阈值为int的最大值
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 容量扩充为原来的2倍
        // 新容量不能超过2^30大小
        // 原来的容量大于等于初始化DEFAULT_INITIAL_CAPACITY=16
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 阈值扩大一倍
            newThr = oldThr << 1; // double threshold
    }
    // 如果原来容量小于等于0, 且原来阈值点大于0
    else if (oldThr > 0) // 原来阈值赋值给新的数组长度
        newCap = oldThr;
    else { // 都不满足的情况下,直接使用默认值
        newCap = DEFAULT_INITIAL_CAPACITY;//16
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 如果原来容量小于等于0, 且原来阈值点大于0情况下,那么newThr是0
    // 计算新的resize最大上限
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    // 新的阀值 默认原来是12 乘以2之后变为24
    threshold = newThr;
    // 创建新的哈希表
    @SuppressWarnings({"rawtypes","unchecked"})
    //newCap是新的数组长度
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    // 判断原来数组是否等于空
    if (oldTab != null) {
        // 把每个桶都移动到新的桶中
        // 遍历原来的哈希表的每个桶,重新计算桶里元素的新位置
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                // 原来的数据赋值为null,便于GC回收
                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;
                     	// 这里来判断如果等于true e这个结点在resize之后不需要移动位置
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        // 原索引+oldCap
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    // 原索引放到bucket里
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    // 原索引+oldCap放到bucket里
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

2.4.5 删除

删除方法就是首先先找到元素的位置,如果是链表就遍历链表找到元素之后删除。如果是用红黑树就遍历树然后找到之后做删除,树小于 6 的时候要转链表。

java 复制代码
public V remove(Object key) {
      Node<K,V> e;
      return (e = removeNode(hash(key), key, null, false, true)) == null ?
          null : e.value;
}

/**
 * @param hash 删除元素的哈希值
 * @param key 删除元素的key
 * @param value 删除元素的值
 * @param matchValue 值是否匹配
 * @param movable 移除元素之后是否移动启动节点
 */
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;
	// 根据hash找到位置 
	// 如果当前key映射到的桶不为空
	// 将桶对应的元素赋值给遍历p
    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 && // 如果桶上的结点就是要找的key,则将node指向该结点
            ((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 {
                // 判断是否以链表方式处理hash冲突,是的话则通过遍历链表来寻找要删除的结点
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        // 比较找到的key的value和要删除的是否匹配
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            // 通过调用红黑树的方法来删除结点
            if (node instanceof TreeNode)
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            else if (node == p)
                // 链表删除
                tab[index] = node.next;
            else
                p.next = node.next;
            // 记录修改次数
            ++modCount;
            // 变动的数量
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

2.4.6 查找元素

java 复制代码
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;
    // 如果哈希表不为空并且key对应的桶上不为空
    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) {
            // 判断是否是红黑树,是的话调用红黑树中的getTreeNode方法获取结点
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                // 不是红黑树的话,那就是链表结构了,通过循环的方法判断链表中是否存在该key
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

三、思考题

3.1 jdk8为何需要引入红黑树?

:jdk1.8之前HashMap的实现是数组+链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。当有大量元素都存放于同一个桶中时,此时这个桶就成为了很长的链表,那么HashMap也就相当于一个单链表,假如单链表有n个元素,遍历的时间复杂度就是O(n),完全失去了它的优势。

针对这种情况,jdk1.8中引入了红黑树(查找时间复杂度为O(logn))来优化这个问题。当链表长度很小的时候,即使遍历,速度也非常快,但是当链表长度不断变长,肯定会对查询性能有一定的影响,所以才需要转成树。

3.2 HashMap中hash函数是怎么实现的?还有哪些hash函数的实现方式?

:对于 key的hashCode做hash操作,无符号右移16位然后做异或运算。 还有平方取中法,伪随机数法和取余数法。这三种效率都比较低。而无符号右移16位异或运算效率是最高的。

3.3 当两个对象的hashCode相等时会怎么样?

:会产生哈希碰撞。若key值内容相同则替换旧的value,否则插入链表末尾,数组长度大于64时,如果链表长度超过阈值8就会转换为红黑树存储。

3.4 什么是哈希碰撞,如何解决哈希碰撞?

:只要两个key计算的数组索引值相同时,就会发生哈希碰撞。jdk8之前使用链表解决哈希碰撞。jdk8之后使用链表 + 红黑树解决哈希碰撞。

3.5 如果两个键的hashCode相同,如何存储键值对?

:通过equals比较内容是否相同。相同,新value会覆盖旧value值,否则将新键值对添加到哈希表中。

3.6 为什么HashMap桶中结点个数超过8才转为红黑树?

答案一

在hashCode离散性很好的情况下,树型bin用到的概率非常小,因为数据均匀分布在每个bin中,几乎不会有bin中链表长度会达到阈值(8)。但是在随机hashCode下,离散性可能会变差,然而JDK又不能阻止用户实现这种不好的hash算法,因此就可能导致不均匀的数据分布。 事实上,随机hashCode算法下所有bin节点的分布频率遵循如下的泊松分布。

在扩容阈值为0.75的情况下,(即使因为扩容而方差很大)遵循着参数平均λ=0.5的泊松分布。可以看到如下的计算概率,一个bin中链表长度达到8个元素的概率为0.00000006,几乎是不可能事件。 c 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

答案二

红黑树的平均查找长度是log(n),如果长度为8,平均查找长度为log(8) = 3。链表的平均查找长度为n/2,当长度为8时,平均查找长虔为8/2 = 4,这才有转换成树的必要;链表长度如果是小于等于6, 6/2 = 3,而 log(6) = 2.6,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。

相关推荐
都叫我大帅哥15 分钟前
🌊 Redis Stream深度探险:从秒杀系统到面试通关
java·redis
都叫我大帅哥16 分钟前
Redis持久化全解析:从健忘症患者到记忆大师的逆袭
java·redis
程序猿阿越35 分钟前
Kafka源码(一)Controller选举与创建Topic
java·后端·源码
程序无bug40 分钟前
Spring6 当中 Bean 的生命周期的详细解析:有五步,有七步,有十步
java
二川bro42 分钟前
飞算智造JavaAI:智能编程革命——AI重构Java开发新范式
java·人工智能·重构
Q_970956391 小时前
java+vue+SpringBoo校园失物招领网站(程序+数据库+报告+部署教程+答辩指导)
java·数据库·vue.js
Wyc724091 小时前
Maven
java·数据库·maven
程序猿小D1 小时前
[附源码+数据库+毕业论文]基于Spring+MyBatis+MySQL+Maven+jsp实现的电影小说网站管理系统,推荐!
java·数据库·mysql·spring·毕业设计·ssm框架·电影小说网站
木头没有瓜2 小时前
idea离线安装插件
java·ide·intellij-idea