HashMap源码解析

基础知识

HashMap是基于哈希表的Map接口的实现,是以key-value存储形式存在,即主要用来存储键值对。HashMap的实现不是同步的,这意味着它不是线程安全的。它的key、value都可以为Null。另外,HashMap中的映射不是有序的,即HashMap无序。

在详细讲解HashMap(JDK7、JDK8对比)之前我们先了解一些基础知识

在不特别注明的情况下,下文JDK7默认指的是JDK7以及之前的版本,JDK8指的是JDK8以及之后的版本。

概念

什么是哈希表?

在说明哈希表之前,咱们也大概了解一下其他的数据结构的知识,主要是在增删改查方面对比。

数组

采用一段连续的存储单元来存储数据。对于指定下标的查找,时间复杂度为O(1);通过给定值进行查找,需要遍历数组,时间复杂度为O(n),当然对于有序数组,可以使用二分法查找、插值查找、斐波那契查找等方式,可以将查找的复杂度提高为O(logn);对于一般的插入删除操作,涉及到数组元素的移动,平均复杂度为O(n)。

线性链表

对于链表的新增,删除等操作(找到指定操作位置后),仅需要处理节点间的引用即可,时间复杂度为O(1),而查找操作需要遍历链表逐一对比,复杂度为O(n)。

二叉树

对于一颗相对平衡的有序二叉树,增删改查,复杂度都为O(logn)。

哈希表

相比以上结构,哈希表进行增、删、查性能很高,不考虑哈希冲突情况,只需要一次定位即可,时间复杂度为O(1)。

所以我们使用哈希表存储数据有很高的增删改查的效率。

如何实现?

我们要了解数据结构的物理存储结构其实就只有两种:

  • 顺序存储结构
  • 链式存储结构

栈、队列、树、图都是从逻辑结构去抽象的概念,映射到内存中,其实也都是这2类物理组织形式。

那么哈希表如何利用这2中物理结构呢?

我们回看数组的结构,只要知道数组的下标就可以快速的找到对应的值,那么哈希表就是利用了这一特性,哈希表的主干就是数组

哈希表数据结构和存在的问题

既然我们知道哈希表的主干是数组,那么数据怎么存储的呢?

哈希函数

比如我们要存储一个元素,我们只需要将元素的关键字,通过某个方法映射到数组中的某个位置,然后将它存储起来。那么我们要查询的时候,只需要再次得到这个位置的下标就可以定位到存储的位置,也就可以快速的取出数据。

存储位置 = function(关键字key)

这个方法就是哈希函数,哈希函数设计的好坏会直接影响到哈希表的优劣。

Hash的音译为“哈希”,直译为散列,是一种信息摘要算法,但是他不是加密。散列算法是一种从任何一种数据中创建小的数字“指纹”的方法。

散列函数把消息或者数据压缩成摘要,使得数据量变小,将数据的格式固定下来。该函数将数据打乱混合,重新创建一个叫做散列值的指纹。

我们平时使用的md5,ssl等都属于hash算法,通过key进行hash计算,就可以获取key对应的hashcode。

image-20200401093114832

哈希表如何存储数据?如上图,假设我们要将key存入下面的一个数组结构中,将key经过hash函数的运算,得到一个存储的内存地址,指向了第2个位置(数组index=2),那么就将key存到这个位置即可。

哈希冲突

万事无完美。

哈希函数毕竟是人为设计的,如果某几个元素经过哈希函数之后得到的值都一样,那么怎么办?当出现这种情况时,我们就说发生了哈希冲突,或者哈希碰撞

大家都听说过哈希冲突,但是没见过实例,这里就列一个:

public class Test {
    public static void main(String[] args) {
        System.out.println("重地".hashCode());
        System.out.println("通话".hashCode());
    }
}
// 打印结果
1179395
1179395

可以看到这2个明显不一样的字符串,经过hash方法之后得到的数值是一样的。

所以哈希函数的设计至关重要,好的哈希函数会尽可能的保证计算简单和散列地址分布是均匀的。但是我们要清楚,数组是一块连续的固定长度的内存空间,它不是无限的,再好的哈希函数也不能保证得到的存储地址绝对不发生冲突。

那么如何解决这个问题呢?哈希冲突的解决方案有很多种:开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,链地址法。

HashMap采用了链地址法,也就是数组+链表的方式**拉链法**(即一系列链表为数组元素组成的数组)。

JDK7:hashmap实现是数组+链表

JDK8:hashmap实现是数组+链表+红黑树

数据结构多重要

下面简单介绍下,还是先画图

image-20200401094322412

如上图,我们存了key到数组了,然后继续插入tom,经过计算得到index=3,将tom存入第四个位置。但是这时候又来了一个lucy,经过hash计算得到的地址也是3(发生了hash碰撞),怎么办。那么就用单向链表将lucy挂到tom的下面去。

JDK7:头插法,在链表头部插入数据

JDK8:尾插法,在链表尾部插入数据

好了,一些基础知识普及完毕,下面开始介绍HashMap的设计逻辑和底层源码分析。

HashMap底层数据结构

下面是一个典型的JDK8的hashmap的数据结构图,我们来一步一步解构它。

image-20200401100322354

开始之前我们写段代码,结合代码来说明下hashmap的设计

public class Test {
    public static void main(String[] args) {
        HashMap<String, Integer> map = new HashMap<>();
        map.put("路飞", 5);
        map.put("索隆", 4);
        map.put("乔巴", 1);
        map.put("路飞", 10);
        System.out.println(map);
    }
}

查看输出

{乔巴=1, 索隆=4, 路飞=10}

可以看到,路飞的悬赏由原来的5亿变成了10亿,而且看到map的打印顺序和我们添加的顺序不一样进一步说明hashmap是无序的。

斗图点赞表情包 厉害了word哥

我们来分析一下这个代码的执行顺序一步一步的解构

HashMap<String, Integer> map = new HashMap<>();

这一句,向内存申请了一块大小为16的数组位置,至于为什么是16,稍后解答。

咱们先画图,(啥也不懂,就先画图)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P4fYjvlI-1585752124102)(http://img.ybq87.top/upic/2020/0401/image-20200401103827517.png)]

  1. 第一句执行后,在内存划了一块16格的内存给他使用(先不着急区分JDK7和JDK8)。

在jdk7的时候,构造方法是创建一个长度为16的Entry<K,V>[] table数组,用来存储键值对。

jdk8的时候,改成了使用Node<K,V>[] table数组来存储键值对,但是数组的初始化是放到了第一次put数据的时候。至于Entry其实就是名称改成了Node,变量名table都没变。

map.put("路飞",5);
  1. 执行这一句的时候,hashmap使用**某个算法**,将路飞这个关键字作为参数传入,然后经过计算得到一个数组的index,假设index=13,那么从数组找到这个13位置,看看这个位置是否有数据,没有数据,就将键值对存进去。

这样看这个某个算法就很重要了,先来窥探一下,当然可以在后面看源码更清晰。

路飞字符串使用String.hasCode()方法得到他的hash值,假设"路飞".hashCode()=1314,然后这个hash会结合数组table的长度length,进行一系列的无符号右移(>>>)、按位异或(^)、按位与(&)操作,计算出数组的索引index

简单来看这个方法就是:index = hash & (length - 1)

那么问题来了!

hashmap计算索引index使用了那么多算法,为什么不直接使用取余的方法?
17%16 --> 1 ;比如17 对数组长度16取余,就是1
22%16 --> 4 ;22对数组长度16取余,就是4,
虽然这2个数计算出来的index不一样,但是当我们扩大插入的数据时,比如插入33
33%16 --> 1 ;出现的hash碰撞,
那么我们再试试另外一种算法,

看看源码简单的(代码是JDK8的,不过JDK7的也一样),下面的方法,在put方法中,调用了putVal,然后我们关注到这个hash(key)

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

查看一下源码:

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

这一句简化一下:key = “路飞”,“路飞”.hashCode() = 1314

int h = key.hashCode();// h = 1314
return (h)^(h >>> 16); // 1314 ^ (1314 >>> 16)
...................我们看看这个计算的结果
1314 = 1024 + 256 + 32 + 2;换算成 二进制就是
0101 0010 0010;
1314 >>> 16 = 0
那么就是
 0101 0010 0010
^0000 0000 0000
------------------(异或^的定义是:相同则结果为0,不同则结果为1)
 1010 1101 1101 = 2781

从而得到了hash = 2781值。按照我给出的简化公式:index = hash & (length - 1)

index = 2781 & 15
    (&的定义:都为1才为1,否则为0)
------------------
 1010 1101 1101 = 2781
&0000 0000 1111 = 15
-------------------
 0000 0000 1101 = 13

最后计算出index = 13,将路飞放入角标为13的数组内。

好了下面解答为什么用异或,与这类的不用%,位运算效率高于%,而且33%16其实结果和33&(16 - 1)相等。

33 % 16 = 1
33 & (16 - 1) = 33 & 15 算一下:
 0010 0001
&0000 1111
------------
 0000 0001 = 1

所以当然选择使用位移等操作,效率高嘛。

老哥,稳。

好了我们继续,

  1. 好不容易执行完了一个put,下面简化一下后面的
map.put("索隆", 4);
map.put("乔巴", 1);

image-20200401132550472

  1. 然后路飞又来了,他的赏金增加了。经过前面的计算我们知道路飞这个关键字经过hash之后,得到的index = 13,然后就会去13这个位置找内存。看看这里有没有数据存储,一看有数据了。下面就开始进一步的比较,已经存储的数据的key的hashcode是否和新传入的一致?再一致的话,就用equals对比下。

可能大家会怀疑为啥还要对比hashcode啊,还记得我们文章开头给出的例子嘛?

对比的结果有2种:

  • 相等:那么就将新的value覆盖已有的value,路飞的悬赏变为了10。
  • 不相等:要是2个equals方法比较出来不相等(没错,我们可以重写equals方法,让路飞不equals路飞),那么就向下找(注意这里是个单向链表),如果都不相等,就在链表尾部加上这个新的key-value。
image-20200401133857047

好了,我们已经摸到了门槛了。

上面的解析只是JDK7的设计逻辑,那么JDK8呢?其实JDK8只是增加一个红黑树。当单向链表里面的数据已经达到了8个,如果又来了一个新的数据要加入到这个链表,那么JDK8会将这个链表转化为红黑树。反之,如果从红黑树删除了元素,当elements.length <= 8的时候,红黑树会转化为单向链表。

好了这块拓展就不深入,这里不讲红黑树,你只要知道红黑树也是一种存储数据的结构,它的查询效率是O(logn)。

面试官:为什么要使用红黑树?单向链表不香嘛?而且为什么是8个之后转为红黑树?
因为在数据量小的时候,单向链表的查询效率很高,随着数据量增加,单向链表的查询时间复杂度是O(n),而红黑树的时间复杂度是O(logn),所以综合考虑选定了8这个节点,将链表转为红黑树。

源码解析(默认JDK8)

我们来看看几个需要认识的关键成员变量

/**
 * 默认初始容量16(必须是2的幂次方),至于为什么是2的幂次,后面讲
 * table的默认初始值,如果在new HashMap()没有指定参数的时候,使用这个值创建数组
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

/**
 * 最大容量,2的30次方,但是注意,并不是说hashmap的table最大只能存储这么多数据,这个参数只是说用户在初始化hashmap的时候,传参最大只允许到 1<<30;
 */
static final int MAXIMUM_CAPACITY = 1 << 30;

/**
 * 默认加载因子,用来计算threshold,为啥是0.75呢?不是1,不是0.5?
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;

/**
 * 链表转成树的阈值,当桶中链表长度大于8时转成树 
   threshold = capacity * loadFactor
 */
static final int TREEIFY_THRESHOLD = 8;

/**
 * 进行resize操作时,若桶中数量少于6则从树转成链表
 */
static final int UNTREEIFY_THRESHOLD = 6;

/**
 * 桶中结构转化为红黑树对应的table的最小大小
 当需要将解决 hash 冲突的链表转变为红黑树时,
 需要判断下此时数组容量,
 若是由于数组容量太小(小于 MIN_TREEIFY_CAPACITY )
 导致的 hash 冲突太多,则不进行链表转变为红黑树操作,
 转为利用 resize() 函数对 hashMap 扩容
 * ---------
 * 用普通人能够理解的话说就是:当数组长度大于64的时候,才将链表转化为红黑树,而不是简单的链表长度大于8了就开始转。
 */
static final int MIN_TREEIFY_CAPACITY = 64;
/**
 保存Node<K,V>节点的数组
 该表在首次使用时初始化,并根据需要调整大小。 分配时,
 长度始终是2的幂。
 */
transient Node<K,V>[] table;

/**
 * 存放具体元素的集
 */
transient Set<Map.Entry<K,V>> entrySet;

/**
 * 记录 hashMap 当前存储的元素的数量,因为并不是每个数组的位置上都会有值,肯定有空位,所以记录下有值的个数
 */
transient int size;

/**
 * 每次更改map结构的计数器
 */
transient int modCount;

/**
 * 临界值 当实际大小size > (容量table.length * 填充因子 DEFAULT_LOAD_FACTOR)超过临界值时,会进行扩容
 */
int threshold;

/**
 * 负载因子:要调整大小的下一个大小值(容量*加载因子)。默认0.75,可以自定义
 */
final float loadFactor;

上面的注释初步将用到的变量进行的说明,但是各个变量怎么用,为什么这么用呢?

我们一步一步的看源码。

image-20200401135831977
如何初始化HashMap

诶,有老铁可能要问了,初始化hashmap这还是个问题嘛?难道不是直接new?这个问题看似简单,其实里面有很多学问,而且我们如果使用IDEA,并且安装了阿里巴巴的代码检查插件,在我们new hashmap的时候,他会提示一句:

image-20200401140149564

下面有个烦人的波浪线,有强迫症的老铁要受不了了。我们就来看看这个构造函数到底什么意思

我们点进源码,从structure看到有4个构造函数

image-20200401140402304

这里我们只研究其中的2个HshMap()HashMap(int)

/**
 * Constructs an empty {@code HashMap} with the default initial capacity
 * (16) and the default load factor (0.75).
 */
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

我们看到默认的构造函数,初始化的时候,将加载因子赋值为0.75。

public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// 调用第三个构造函数,加载因子默认 0.75,
/**
 * 传入初始容量大小和负载因子 来初始化HashMap对象
 */
public HashMap(int initialCapacity, float loadFactor) {
    // 初始容量不能小于0,否则报错
    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;
    // 初始化threshold大小
    this.threshold = tableSizeFor(initialCapacity);
}

这里我们看到可以接受一个initialCapacity参数,假设我们这么写:

HashMap<String,Integer> map = new HashMap<>(10)

initialCapacity = 10,进入了tableSizeFor(initialCapacity)方法

/**
 * 找到大于或等于 cap 的最小2的整数次幂的数。
 */
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;
}

按照这个算法,10的最接近的2的幂次是16,所以this.threshold = 16

那么这个threshold是什么玩意?后面就会见到了,这里按下不表,我们只需要知道你传入的参数最后不一定就真的是用到的实际数值。

DEFAULT_INITIAL_CAPACITY 为何必须是2的幂次方

看一段代码,我们假设下面的定义是创建一个长度为7的table数组

HashMap<String,Integer> map = new HashMap<>(7)
map.put("路飞",5)

在之前我们讲解hashmap的数据结构的时候,我们知道在hashmap中添加一个数据,需要计算key的hash值,然后按照某种规则得到他在数组中的index。

之前我们也得到了这个公式index = hash & (length - 1)

现在 length = 7,length = 16,我们,取几个数分别对比一下结果

@Test
public void testL() {
    Set<Integer> nums = new HashSet<>();
    for (int i = 0; i < 20; i++) {
        int hash = org.apache.commons.lang3.RandomUtils.nextInt(1000, 5000);
        nums.add(hash);
    }
    for (Integer hash : nums) {
        System.out.println((hash & (7 - 1)) + "<====>" + (hash & (16 - 1)));
    }
}
打印:
2<====>3
2<====>3
6<====>6
6<====>6
0<====>9
2<====>10
2<====>11
2<====>11
4<====>12
4<====>13
4<====>13
4<====>13
6<====>15
0<====>1
2<====>2
4<====>4
0<====>8
0<====>9
2<====>10
2<====>11

可以观察到,基本上右侧的数据都是很均匀的占用了1~15这些位置,但是左边的只有 0、2、4、6这些,利用效率非常低。

我们就得出一个结论,之所以设置为2的幂次,其实是为了让hash值均匀的分布在数组上,从而充分的利用空间,而一些非2幂次的数虽然也可以用,但是使用效率不高。

为何加载因子是0.75

加载因子是什么,有什么作用?我们首先要知道一个概念,hashmap中的table数组因为是有初始容量的,当我们要存储的数据大于这个容量怎么办呢?就需要扩容了。

但是扩容不是盲目的扩容,应该选择一个合理的计算手段,不然就浪费内存,内存资源还是比较紧张的。

而这个扩容因子就和扩容有关。

假设有语句

HashMap<String,Integer> map = new HashMap<>()

成员变量就都是默认的,那么加载因子为0.75,默认长度为16,当table中的数据size > (16 * 0.75)的时候,table就要进行扩容。

此时的各类参数的值为:

threshold = 16
loadFactor = DEFAULT_LOAD_FACTOR = 0.75
DEFAULT_INITIAL_CAPACITY = 16

那么为什么要定义这个加载因子是0.75呢?不是1,不是0.5,0.7?

其实还是从效率上来考虑,假设设置为1,那么就是16个位置全部占满了才扩容。我们知道hash计算是会出现碰撞的,那么就会产生链表结构,链表再快也没有数组中直接通过角标获取的快吧。所以为了尽量使数据落在数组上,要减少链表,那么就不要用1了。我们用0.7可以吗?

16 * 0.7 = 11.2,这样计算出来还是个float,我们划空间不可能给小数,你说取整就好了。那为什么不一开始就定为0.75,这样计算出来的全部是整数。因为 数组的初始长度都是2的幂次,2^n * 0.75结果都是整数,不需要其他计算了。

那么有人说设置的小一点如何,0.5?

那就是16 * 0.5 = 8,占用了8个位置就要划新的区域给他,这个hashmap也太吃内存了吧。要知道扩容的时候是之前的空间的倍增方式扩容的。

newThr = oldThr << 1; // double threshold

16 << 1 = 32;万一你只需要存储9个数据,结果使用0.5因子,划了32个空间给他,多浪费。

所以这个0.75其实是一个综合考虑性能和内存空间的结果。

除非你非常确定你的代码中数据占用的位置,否则还是使用默认的构造函数吧

put方法

下面分析一下put方法都是什么原理

public V put(K key, V value) {
    // 调用hash(key)方法来计算hash ,这里我们上面看到了:hashcode ^ (hashcode >>> 16)
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; 
    Node<K,V> p; 
    int n, // 记录数组长度
    	i;
    // 容量初始化:当table为空,则调用resize()方法来初始化容器table
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    //确定元素存放在哪个桶中,桶为空,新生成结点放入桶中,这里就是计算hash值得到数组的index,存数据
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        // 比较桶中第一个元素(数组中的结点)的hash值相等,key相等
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            //如果键的值以及节点 hash 等于链表中的第一个键值对节点时,则将 e 指向该键值对
            e = p;
        // 如果桶中的引用类型为 TreeNode,则调用红黑树的插入方法
        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) {
                    //在尾部插入新结点
                    p.next = newNode(hash, key, value, null);
                    // 如果结点数量达到阈值,转化为红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                // 判断链表中结点的key值与插入的元素的key值是否相等
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        //判断要插入的键值对是否存在 HashMap 中
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            // onlyIfAbsent 表示是否仅在 oldValue 为 null 的情况下更新键值对的值
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    // 键值对数量超过阈值时,则进行扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}
resize方法和扩容机制

table的默认长度是2的幂次,而默认阈值threshold = 16 * 0.75 = 12,当table中数据个数大于12的时候,就要进行扩容。

数组扩容按照当前table的长度的2倍进行,比如当前16,就扩容到32,相应的threshold = 32 * 0.75 = 24了。

扩容后,重新计算键值对的位置,然后移动他们到合适的位置。

final Node<K,V>[] resize() {
    // 拿到数组桶,刚new hashmap,啥都没有 table = null,threshold = null
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;// 数组长度
    int oldThr = threshold;// 还记得我们上面介绍的嘛?threshold ,不过因为我们没有在构造函数传值,所以这里threshold = null
    int newCap, newThr = 0;
    // 如果数组桶的容量大于0,显然第一次put的时候 oldCap = 0,我们看else if 方法去
    if (oldCap > 0) {
        // 如果比最大值还大,则赋值为最大值
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 如果扩容后小于最大值 而且 旧数组桶大于初始容量16, 阈值左移1(扩大2倍)
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 左移1位
            newThr = oldThr << 1; // double threshold
    }
    // 如果数组桶容量<=0 且 旧阈值 >0
    else if (oldThr > 0) // 这里的 oldThr = threshold = 16
        // 新容量=旧阈值
        newCap = oldThr;
    // 如果数组桶容量<=0 且 旧阈值 <=0
    else {               // zero initial threshold signifies using defaults
        // 新容量=默认容量
        newCap = DEFAULT_INITIAL_CAPACITY;
        // 新阈值= 负载因子 * 默认容量 = 0.75 * 16 = 12
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 如果新阈值为0
    if (newThr == 0) {
        // 重新计算阈值
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    // 更新阈值
    threshold = newThr; // 初始化的时候 threshold = 16了。
    @SuppressWarnings({"rawtypes","unchecked"})
        // 创建新数组,啊哈,看到了吧,创建数组的时候用到了 newCap = oldThr = threshold = 16
        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) {
                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 { // preserve order
                    // 如果不是红黑树,则按链表处理
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    // 遍历链表,并将链表节点按原顺序进行分组
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            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);
                    // 将分组后的链表映射到新桶中
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}
treeifybin方法转为红黑树
final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
    // 先去判定当前的table长度是否大于64,小于64位的数组,优先进行扩容
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    // 如果节点不为null,将node节点转为 treenode 加入到树中,
    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);
    }
}
get方法

查找方法就相对简单很多了,

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}
// 获取hash值
static final int hash(Object key) {
    int h;
    // 拿到key的hash值后与其五符号右移16位取与
    // 通过这种方式,让高位数据与低位数据进行异或,以此加大低位信息的随机性,变相的让高位数据参与到计算中。
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

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) {
        // 判断桶中第一项(数组元素)相等
        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 {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}
image-20200401162307913

老铁们关注一波
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值