深度解读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,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。

相关推荐
安之若素^几秒前
启用不安全的HTTP方法
java·开发语言
ruanjiananquan997 分钟前
c,c++语言的栈内存、堆内存及任意读写内存
java·c语言·c++
chuanauc34 分钟前
Kubernets K8s 学习
java·学习·kubernetes
一头生产的驴1 小时前
java整合itext pdf实现自定义PDF文件格式导出
java·spring boot·pdf·itextpdf
YuTaoShao1 小时前
【LeetCode 热题 100】73. 矩阵置零——(解法二)空间复杂度 O(1)
java·算法·leetcode·矩阵
zzywxc7871 小时前
AI 正在深度重构软件开发的底层逻辑和全生命周期,从技术演进、流程重构和未来趋势三个维度进行系统性分析
java·大数据·开发语言·人工智能·spring
YuTaoShao3 小时前
【LeetCode 热题 100】56. 合并区间——排序+遍历
java·算法·leetcode·职场和发展
程序员张33 小时前
SpringBoot计时一次请求耗时
java·spring boot·后端
llwszx6 小时前
深入理解Java锁原理(一):偏向锁的设计原理与性能优化
java·spring··偏向锁
云泽野7 小时前
【Java|集合类】list遍历的6种方式
java·python·list