三顾HashMap,一顾结构,二顾变量,三顾构造函数及首次扩容原理

背景摘要:在Map集合中,最常用的集合就是HashMap集合了。相信各位也能脱口而出她的特征,JDK7(以下简称为7)和JDK8(以下简称为8)源码和实现不一样。7底层由数组+单向链表实现。在这之前我们提到过基于数组和链表实现的两个集合。ArrayListLinkedList。那么在8源码中新增了红黑树这么一个数据结构,由于其特性大大增加了查询效率。同时HashMap也是无序且线程非安全。那么今天基于7的源码来三顾HashMap。一顾结构解析、二顾核心变量、三顾构造函数及首次扩容原理。

HashMap

目录

一、初探 HashMap结构解析

二、再探 HashMap核心变量

三、三顾 HashMap构造函数及首次扩容

构造函数

首次扩容


 一、初探HashMap,结构解析

HashMap,她的构造为单向链表与数组,链表的概念可参考 LinkedList 一文。首先我们先一张图简单看看她结构。

图1.1

 如图1.1,我们可以看到HashMap是一个由数组装载链表组成的集合。大家都知道HashMap初始扩容值默认为16,那么就代表初次扩容时,数组里是含有16个空的链表的。

二、再探HashMap,核心变量

基于JDK1.7讲解,多个JDK安装切换方法请看 IDEA将当前项目JDK更改为指定版本

了解了结构之后我们再来看她重要的变量

码云集合源码分析项目地址:

https://gitee.com/yiang-hz/gather

https://gitee.com/yiang-hz/gather/tree/master/map/src/main/java/com/yiang/map 

/**
 * 默认初始容量 必须是2次幂 这里为 16 二进制
 * // aka 16
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
/**
 * 默认的负载因子  也就是扩容倍数
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
 * 最大扩容值,如果超过了则为该值 10个亿
 */
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
 * 扩展因子
 */
final float loadFactor;
/**
 * 阈值,需要扩容时设置,实际HashMap存放的大小 当达到该阈值时进行扩容
 * 要调整大小的下一个大小值(容量*负载因子)
 * 如果表为空,那么表将会在扩容时创建
 */
int threshold;
DEFAULT_INITIAL_CAPACITY:默认初始容量,默认值为16。必须为2的次幂。通过1<<4二进制形式计算在计算机内是最快的。
DEFAULT_LOAD_FACTOR:默认负载因子,默认值为0.75。即扩容倍数。
MAXIMUM_CAPACITY:最大扩容值,也就是2的30次幂。10个亿。一般不存在超过该值情况。
loadFactor:负载因子,该值直接影响HashMap每次扩容后的容量。构造函数初始化该值。
threshold:阈值,构造函数初始化赋值。HashMap实际存放元素大小。计算公式为(容量*负载因子)。
--构造赋值为设置的初始容量(DEFAULT_INITIAL_CAPACITY),默认16。
--首次扩容为16*0.75=12。二次扩容为 16*2=32(实际大小) -> 32 * 0.75 = 24(阈值)
/**
 * 一个空的实例,扩容前共享
 * 实际上定义该空值用来转换table,可读性更强。
 */
static final YiangHashMap.Entry<?,?>[] EMPTY_TABLE = {};
/**
 * 初始化,Entry对象数组
 */
transient YiangHashMap.Entry<K,V>[] table = (YiangHashMap.Entry<K,V>[]) EMPTY_TABLE;
/**
 * 数据表中包含的键-值映射的数目。即数组大小
 */
transient int size;
EMPTY_TABLE:一个空的数组,没有实际意义,用来增加可读性
table:数据表。也就是图1.1示列的数组。首次扩容时即为16。二次扩容为32。数组大小以2倍扩容。
size:数组大小,即数组中实际存放的链表数量。

以上即核心八个变量,下面再解剖一下构造函数的赋值,及首次扩容。

三、三顾HashMap,构造函数及首次扩容

HashMap源码中,通过无参与有参构造方法,来实现可自定义初始容量的方式来使用HashMap,那么来一探究竟

3.1、构造函数

无参构造:初始容量为默认值16,负载因子为默认值0.75。

public YiangHashMap() {
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}

单参构造:传递自定义初始容量。负载因子为默认值0.75。

public YiangHashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

那么重点讲通过重载利用的传递初始大小,与负载因子的方法。

  1. 首先判断初始容量是否小于0,如果小于抛出异常。
  2. 再次判断是否大于最大扩容值,如果超过了该扩容值,则默认为最大扩容值。
  3. 判断扩容倍数值是否为符合标准值,即正数。
  4. 设置加载因子(loadFactor)为设置的加载因子(未传递为默认0.75)。
  5. 设置阈值(threshold )为初始容量。(未传递为默认16)。
  6. init()方法,钩子函数,当前类未实现,给予子类实现其它操作。
    /**
     * HashMap初始化构造函数源码解析
     * @param initialCapacity 初始化扩展容量 默认 16
     * @param loadFactor 加载因子默认 0.75
     */
    public YiangHashMap(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;
        threshold = initialCapacity;
        //子类的初始化钩子。没有实现的方法,给予子类继承实现其他操作。模板模式
        //init();
    }

看完之后我们可能会有疑问:为什么阈值在构造函数赋值会为16?有什么含义吗?为什么不是数组大小的0.75倍(12)?

还有这事?那我们看看接下来的首次扩容,来了解其原因。

3.2、首次扩容

总所周知,HashMap是在存值时开始扩容的,也就是说在构造函数执行完后,数组的大小还是一个默认值空对象的。那么在源码put方法中有这么一段代码。如果table为空,那么开始初始化数组。也就代表数组是在第一次存值时开始扩容的。那么第一次扩容时,扩容大小的size 是赋值给阈值(threshold)的,在首次扩容时正是传递了该参数。所以我们了解阈值在构造参数时做了一个承载数组大小的作用。

//如果table为空,那么初始化数组大小
if (table == EMPTY_TABLE) {
    inflateTable(threshold);
}

再来看看扩容方法执行的步骤:

  1. 通过内部方法(三元表达式)判断是否大于最大值,并且判断是否小于1。范围在 1 < size < max。传递默认16得出16
  2. 计算阈值。当前扩容值*扩展因子 默认是16*0.75。这里做了三元判断,能够得出最大值为默认最大值(MAXIMUM_CAPACITY)+1。而目前是未超过最大值,故为12。
  3. 表格初始扩容为构造方法初始设置扩容值大小。 默认:16
/**
 * 扩容table -- Inflates the table.
 * @param toSize 大小
 */
private void inflateTable(int toSize) {
    // Find a power of 2 >= toSize 计算值,三元的规约判断 计算
    // 结果: 1 < n < Max 三种情况 所以首次扩容这里为16
    int capacity = roundUpToPowerOf2(toSize);
    //计算最小值: 16 * 0.75 = 12 , 最大值 10亿 + 1 那么threshold首次扩容值就是12
    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    //表格以capacity大小进行初始化,这里capacity为16
    table = new YiangHashMap.Entry[capacity];
}

通过以上扩容源码我们了解,阈值(threshold)虽然为数组大小的0.75,但在构造方法执行完成时,是为首次扩容大小的,而数组实际上是一个空数组。在首次扩容时,将扩容大小赋值给数组。然后将该值通过与负载因子计算得出阈值的。这也就诠释了上文“为什么阈值在构造函数赋值会为16?”这个问题。


总结:

HashMap的结构由单向链表 + 数组组成。扩容因子默认为0.75,数组初始容量默认为16,但实际存放大小(阈值)默认为12。最大扩容值为2^30。扩容是在首次存放键值入集合时开始,而不是在创建时就进行扩容。创建时只针对初始容量赋值给阈值做承载。并赋值对应的加载因子。且包含子类实现的钩子函数init()。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章