Java之HashMap源码分析(第一篇:创建对象)

(注意:本文基于JDK1.8 )

前言

    HashMap是Java中最常用的容器类(集合类)之一,表示的数据结构是哈希表,既然是容器,它的作用当然是存储元素,由于Java是纯面向对象的计算机高级语言,所以存储的每个元素必须是对象,当然实际存储的仅仅是对象的引用。

    数组使用下标获取元素(也称读取元素),而HashMap使用Key对象获取元素(读取元素),这就是HashMap的特点!HashMap如何实现使用Key对象查找到一个元素呢?又是如何在内存空间不足时扩充容量以确保存储更多的元素?还有各种其他问题?带着这些疑问,我开始对HashMap的源码进行了分析。

    本文将先从创建一个HashMap对象开始,当然创建HashMap对象前,HashMap类会被先加载到虚拟机内存中(注意:文本不涉及基类的加载),那我们看看HashMap类加载时,执行了哪些代码?

HashMap类加载

private static final long serialVersionUID = 362498820763181265L; //HashMap类持有序列化ID
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //HashMap类持有的底层数组默认容量
static final int MAXIMUM_CAPACITY = 1 << 30; //底层数组最大容量
static final float DEFAULT_LOAD_FACTOR = 0.75f; //默认负载因子值
static final int TREEIFY_THRESHOLD = 8; //单链表转红黑树结构的阈值,(转红黑树的第1个条件)
static final int UNTREEIFY_THRESHOLD = 6; //红黑树结构转单链表的阈值
static final int MIN_TREEIFY_CAPACITY = 64; //转红黑树需要的最小数组容量,(转红黑树的第2个条件)

    当第一次使用HashMap类时,属于类的静态变量、常量将加载到虚拟机内存中(注意:静态内部类只有使用才加载),它们就是一些常用的值,可见都是常量值,HashMap会使用

备注1:静态变量、常量均是在类加载时执行

HashMap实例变量加载

transient Set<Map.Entry<K,V>> entrySet; //HashMap对象持有的Set集合对象(第一次遍历HashMap时会创建该对象)
transient Node<K,V>[] table; //每个HashMap对象持有的底层数组对象,元素类型为Node
transient int size; //HashMap对象持有的实际元素总数(由size变量保存)
transient int modCount; //HashMap对象持有的用于实现fail-fast机制的实例变量,添加元素、删除元素(包含clear)时,modCount值会被改变
int threshold; //HashMap对象持有的扩容阈值
final float loadFactor; //HashMap对象持有的负载因子

    当第一次创建HashMap对象,实例成员将优先与构造方法执行,这些HashMap对象持有的实例变量会在后面的代码分析中,经常提起,这里我们先有一个熟悉

    transient修饰的字段不参与对象序列化,反序列化对象时,未参与的实例变量将会返回默认的零值:比如基本数据类型为0、引用数据类型为null

备注2:实例变量,是在创建对象时执行

创建HashMap对象的4个构造方法

    用于创建HashMap对象的4个构造方法,你根据需求,可以使用其中任意1个构造方法,接下来逐个分析每个构造方法是如何实现的?

HashMap无参数的构造方法(最常用)分析

    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

    用于创建HashMap对象的构造方法,默认构造方法是我们最常用的构造方法!接下来看看它的方法体是如何实现的?

1、将称为负载因子的loadFactor赋初始值

首先将HashMap对象持有的实例变量loadFactor赋值为0.75f,常量DEFAULT_LOAD_FACTOR的值为0.75f,loadFactor是HashMap对象持有一个float类型的实例变量,因为它以final修饰,所以在创建HashMap对象时就必须对loadFactor进行初始化,另外loadFactor由于使用final修饰,是不能在内存中运行时二次修改loadFator值的,说明创建一个HashMap对象后,它的负载因子不能再改变。

说明:loadFactor在HashMap中被称作负载因子(英文名:load factor),该实例变量的作用:HashMap对象持有的底层数组对象在每次扩充容量前,都会和一个扩容阈值进行比较,而这个扩容阈值是怎么计算出来的呢?

答:底层数组对象的容量与loadFactor的值相乘,可计算出扩容阈值

【公式】: 底层数组对象容量 * 负载因子loadFactor = 扩容阈值

    static final float DEFAULT_LOAD_FACTOR = 0.75f;

DEFAULT_LOAD_FACTOR是HashMap类持有的一个常量,它的值是0.75f,也称为默认的负载因子值

举个例子:假设数组容量为12,12 * 0.75 = 9,说明HashMap对象持有的底层数组对象的扩容阈值为9,当底层数组对象已存储的元素数量大于等于9时,HashMap对象持有的底层数组对象必须扩充容量以此存储更多的元素

HashMap一个参数的构造方法

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

    可以指定底层数组容量,创建HashMap对象(当你可以预估出存储的元素总数,则可使用这个构造方法,成本低),方法体的实现如下:

1、直接调用2个参数的构造方法

    该方法内部直接调用HashMap两个参数的构造方法(见本文下方),并向其传入用于初始化数组容量的initialCapacity值,另外一个传入的参数是默认的负载因子值0.75f。

    备注:DEFAULT_LOAD_FACTOR为常量,它的值是0.75f!

HashMap两个参数的构造方法分析

    public HashMap(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;
        this.threshold = tableSizeFor(initialCapacity);
    }

    创建HashMap对象时,不仅可以指定数组的默认容量,也可以指定一个负载因子,这样我们就可以控制扩容阈值了……( 公式:数组容量 * 此负载因子 == 扩容阈值)

1、检查指定的数组容量值是否合理

    首先对传入的int值initialCapacity进行检查,intialCapacity表示底层数组的初始化容量,如果指定的容量小于0,直接抛出一个IllegalAugumentException告知调用者,因为数组的容量不可以小于0,这个容错必不可少!

2、检查指定的数组容量是否超出的最大容量

    通过数组容量的检查后,继续检查表示数组初始化容量的int值initialCapacity是否超过MAXIMUM_CAPACITY,如果超过最大容量,则直接使用MAXIMUM_CAPACITY作为数组的容量

3、检查负载因子是否合理

    负载因子loadFactor容错,先检查loadFactor是否小于0、接着又判断loadFactor是不是数字(NaN 实际上就是 Not a Number的简称),只要有一个条件不满足就会抛出一个IllegalArgumentException异常对象(这个异常对象用来描述参数是否合理)。loadFactor的检查全部通过之后,再将传入的局部变量loadFactor赋值给HashMap对象持有的实例变量loadFactor负责保存(个人觉得这里少一个检查,loadFactor应该不可以大于1……,我终于找到大佬的bug了)

4、初始化负载因子与二次计算数组容量(此处暂时作为默认阈值,后面会对扩充阈值重新计算)

    然后又调用一个tableSizeFor()方法(见本文下方),该方法接受传入的表示数组初始容量的initialCapacity变量,tableSizeFor方法返回一个与传入值initialCapacity最接近的一个2^n次幂的值(等于或大于),最终tableSizeFor()方法的返回值会赋值给HashMap对象持有的threshold进行持有(threshold表示HashMap对象持有的底层数组对象的需要扩容时的实际阈值)

静态方法tableSizeFor(int)分析

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

    用于确保HashMap对象持有的数组对象的容量永远是2的n次方的方法,无论你传入的数组容量是几,静态方法tableSizeFor()的内部都会做对数组容量值的二次计算,而计算后的值一定是最接近于2的n次方的值(为什么一定得是2的n次方呢?),一起看下方法体如何做到这个的?

1、传入的容量先减去1

    表示数组容量的cap值传入到该方法后,先减去1的计算,接着将结果赋值给临时局部变量n进行存储

2、烧脑的计算过程……(带入值5进行计算)

    下面的计算过程烧脑,所以我带了一个数字进去,此处为局部变量cap传入的数字是5,表示数组容量为5,看看计算过程,它是如何找到一个最接近于5,且大于5,又是2的n次方的数字的……

传入值cap == 5

n: 4 (int n = cap - 1)

00000100(n)

00000010(n >>> 1)

00000110(n与n>>>1 按位或计算,计算结果6会再赋值给n)

n:6

00000110(n)

00000001(n >>> 2)

00000111(n与n>>>2按位或计算,计算结果7会再赋值给n)

n:7

00000111(n)

00000000(n >>> 4)

00000111(n与n>>>4按位或计算,计算结果7会再赋值给n)

n:7

00000111(n)

00000000(n >>> 8)

00000111(n与n>>>8进行按位或计算的结果,计算结果7会再赋值给n)

n:7

00000111(n)

00000000(n >>> 16)

00000111(n与n>>>16进行按位或计算的结果,计算结果7会再赋值给n)

终于到return语句了,内部做了两个判断

第一:当n小于0时,这里直接return 1

第二:当n大于0时,上文举例的n值为7,首先用n与MAXIMUM_CAPACITY最大容量值进行比较,当n大于等于最大容量值MAXIMUM_CAPACITY时,n会被赋值为MAXIMUM_CAPACITY值,此时我们的n是7,就会走n+1,我们的n加上1,那么最终结果是8,数字8是最接近传入数组容量5,且大于5,又是2的n次幂方(8是2的3次方)(注意:2的n次方数,必须大于传入值,所以不能是数字4)

HashMap接受一个参数的构造方法

    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

    创建HashMap对象时可以指定另一个Map,Map中的所有元素会被直接添加到新创建的HashMap对象中,此时创建的HashMap对象自带元素……

1、初始化负载因子

首先为负载因子loadFactor赋值为默认值0.75f

2、调用实际添加元素的方法

接着调用putMapEntries方法(见本文下方),并同时将传入的Map对象m和一个boolean值false传了进去

注意:传入的Map对象的类型参数必须与当前的HashMap的类型参数保持一致(说明Key与Value的类型必须一致):即传进来的Map对象的Key对象必须是范型K或K的子类型,Value对象则必须是范型V或V的子类型

HashMap对象持有的数组对象,数组对象中实际缓存的是1个个的Node对象,而每个Node对象持有Key对象和Value对象,所以Key类型需要保持一致,但为何Value类型也要保持一致呢?不一致的话,Map如何合并元素呢?

思考:第一个元素Key传入的是Person对象(父类对象),第二个Key传入的是Man对象(继承了Person的子类对象),Person与子类Man要使用同一个equals()方法、同一个hashCode()方法吗?必须得使用同一套equals()方法、hashCode()方法呀,不然怎么比较Key对象是否相同呢?

putMapEntries()方法分析

    final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        int s = m.size();
        if (s > 0) {
            if (table == null) { // pre-size
                float ft = ((float)s / loadFactor) + 1.0F;
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
                if (t > threshold)
                    threshold = tableSizeFor(t);
            }
            else if (s > threshold)
                resize();
            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);
            }
        }
    }

    用于添加多个元素的方法(多个元素保存在传入的Map对象中),传入的第一个参数为Map对象,表示持有多个元素的容器对象,第二个参数表示啥来着,继续看源码呗

1、检查Map对象持有元素的总数

首先获得传入的Map对象的元素总数,然后赋值给临时变量s存储,如果变量s的元素总数大于0,才会继续进行合并元素的行为(说明只有传入的Map保存着多个元素时,才有必要做合并元素的工作)

2、合并元素前的检查,对两种情况分别处理,一种是合并元素情况进行数组容量初始化工作、另一种是添加多个元素的是否需要底层数组扩容

进入合并逻辑内部后,首先判断当前HashMap对象持有的table数组是否为空(第一次创建HashMap对象时,这里的table一定为null),为空时,会先用传入的Map对象的元素总数s与已经赋值的加载因子0.75相除,接着又加了一个1,结果将被赋值给局部变量ft进行保存,再将变量ft的值与最大容量进行比较,若ft小于最大容量,则赋值给一个局部t进行保持,若变量ft的值已经超过最大容量,t将会被存储为最大容量(这里是对传入Map的容量做了一个保护,可能传入Map的元素数量特别大,40个亿个,现在不会出现,但是50年后,计算机牛比了,这种情况有可能出现,作者还真是严谨),局部变量t接着会和当前Hash的扩容阈值threshold进行比较,大于时,会将变量t的值传入tableSizeFor方法(见3号知识点),tableSizeFor的返回值将交由实例变量threshold进行持有;当table不为null时,证明该putMapEntries方法并不是通过HashMap构造方法调用的,而是通过putAll方法调用的,在HashMap中,就只有这两处调用了这个方法,继续table不为null的情况,它会先判断Map的元素总数s是否大于了当前HashMap的扩容阈值,如果大于,就会调用resize()方法进行扩容底层数组,不大于的话,当然是不用扩容底层数组了。

3、实际添加元素过程

最后执行到一个for each语句中(遍历每一个Node对象),该for each语句中,会先通过entrySet()方法获得一个Set对象,Set对象是一个集合,它肯定实现了iterator方法,for each会自动再调用Set的iterator方法,获取到一个Iterator对象,接着内部会利用Iterator对象的hasNext方法判断当前是否有元素,有的话就会调用Iterator的next方法从而获得一个Map.Entry对象,Map.Entry是一个接口,HashMap中的实际结点对象实现了该接口,所以这里的for循环做的就是传入Map对象的遍历,一个元素一个元素的遍历,每次访问一个元素的时候,都会调用putVal方法,将其存储到HashMap的底层数组中,可见HashMap添加一个Map进来的时候,效率真的不高,只能一个元素一个元素的添加进来,倘若传入的HashMap很大,这里的效率会很低,可见putAll的效率也不怎么样

总结

1、HashMap对象持有的数组对象的容量,一定为2的n次方,当你传入的容量值不是2的n次方时,有个静态方法tableSizeFor()的会做二次计算,该方法会计算出一个与你传入值最接近的、且大于或等于2的n次幂的值,比如你传入的容量是17,则静态方法tableSizeFor()会返回容量32(2的5次方),而不会是16,因为16小于17!

2、HashMap对象刚创建时,持有的底层数组对象仍未创建,而是在第一次添加元素的时候,由resize()方法创建数组对象

3、HashMap与另一个Map进行合并时(HashMap添加多个元素),另一个Map中的元素是逐个添加到当前HashMap对象持有的底层数组对象中的,添加的效率并不高

4、HashMap中的静态方法tableSizeFor()写的真好,它的作用是保证HashMap对象持有的数组对象的容量一定是2的n次方

5、HashMap对象创建时,HashMap对象持有的扩容阈值threshold与持有的底层数组对象的容量是一样的,当再次使用HashMap添加元素时,会二次更新此threshold值,这里是巧妙的临时使用threshold保存值的一个方法

6、对象序列化技术可用于clone一个相同状态的对象,HashMap持有的几个实例变量并不参与对象的序列化过程,那么在HashMap做对象反序列化时,能不能得到一个现有HashMap对象的复制品呢?HashMap显式标记支持Cloneable接口?后面请看HashMap序列化的单独文章

7、HashMap对象持有的底层数组对象的每一个元素位置都称为桶(因为数组的下标处可以存放1个单链表或1个红黑树而得名【当然存储的是某个结点对象的引用】),所以这个底层数组也被称作桶数组……

8、思考一个问题,为什么底层数组对象的容量必须是2的n次方呢?为啥呢?继续看源码呗,下一篇将分析添加元素的方法!

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值