【Java集合篇】jdk7的HashMap源码分析

前言:

如果面试官问你:
说说你对HashMap的理解?

是不是现在感觉有些摸不着头绪,没事,接下来,笔者就带着大家一起深度剖析Java7Java8中HashMap的底层实现。

Java8的HashMap源码分析链接

版本声明:本文的Java版本:jdk-7u7-windows-x64

一.HashMap底层是怎么存储的?Entry是什么?

1.默认大小:

下面的代码就是Java中HashMap的底层实现——一个Entry类型的数组,默认长度是16.

    /**
     * The hash table data.
     */
    private transient Entry<K,V>[] table;
    
	static final int DEFAULT_INITIAL_CAPACITY = 16;

2.Entry是什么?

    static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;

        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }

上面的代码来自Java源码,可以看出Entry是一个静态内部类 (外部类是HashMap),并且提供了如下的四个成员变量。
(1)、一个带final的key值 (不可变) ,类型是一个泛型K
(2)、一个参数类型是泛型V的 value值
(3)、一个Entry<K,V>类型的next变量,(确切的理解一个指针,或者是一个地址引用)
(4)、一个int类型的哈希值。

3.为什么Java7的HashMap是数组+链表

通过阅读这一段短暂的源码,发现HashMap底层是一个数组,而数值中存储的是一个一个的Entry对象,而且每一个Entry对象都有一个next的成员变量,这个成员变量,存储的是一个Entry对象的引用。

总结

  • 1.因为底层有一个Entry类型的数组,所以HashMap底层是由数组构成
  • 2.因为数组中每一个存储的Entry对象,其对象的成员变量中都有一个 next的成员变量,所以也就构成了链表

疑问 ?

  • 有的读者可能听说过,HashMap不是数组+链表+红黑树吗?
    • Java7是数组+链表
    • Java8是数组+链表+红黑树
  • 因为是链表结构,所以新元素总会有一个存储的位置问题。
    • Java7新添加的元素在最上面,就是新添加的元素在数组上,然后把原来数组上的引用赋值给新添加的next成员变量:即:新添加的的元素取代数组中Entry1的位置,而新添加的元素的next成员变量存储Entry1的地址值。
    • Java8是直接将新元素的成员变量,直接赋值给链表上next成员变量为null的。即:新添加的元素的地址值赋值给Entry2的next成员变量。

在这里插入图片描述

二、构造器

1.调用无参构造器,底层数组长度是多少?

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

分析:当我们调用无参数的构造器的时候,我们实际上携带两个默认参数去调用两个参数对应的构造器。

那么,我i们就看看这两个默认参数是什么?

  • 底层数组的默认长度
  • 认参数为加载因子,后续谈到扩容条件的时候,会用到它,现在知道即可.
	//底层数组的默认长度
    static final int DEFAULT_INITIAL_CAPACITY = 16;
    //默认加载因子,后续的扩容中会用到它,暂且记住即可。
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

2.调用一个参数的构造器,指定长度为15,底层数组长度是15吗?

分析:当我们调用一个参数的构造器的时候,我们实际上携带一个指定的参数,一个默认参数去调用两个参数对应的构造器。

    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
  • 指定的参数即为:我们指定的默认长度
  • 默认参数为加载因子,后续谈到扩容条件的时候,会用到它,现在知道即可.
    //默认加载因子,后续的扩容中会用到它,暂且记住即可。
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

3.核心构造器,答案就在其中。。

提示:我们发现上面的两个构造器,其实最终都是调用的两个参数的构造器 (第一个参数是底层数组的默认长度,第二个长度是加载因子) ,那么我们就来看看这个两个参数的构造器里面有什么特别之处。

为了提示方便,所以把代码块分成了5部分,即为代码块0到代码块4.


public HashMap(int initialCapacity, float loadFactor) {
		//代码块0开始========================================
	        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);
		
		
		//代码块1开始========================================
        // Find a power of 2 >= initialCapacity
	        int capacity = 1;
	        while (capacity < initialCapacity)
	            capacity <<= 1;
		
		
		//代码块2开始========================================
	        this.loadFactor = loadFactor;
	        threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        
        
        //代码块3开始========================================
        	table = new Entry[capacity];


        useAltHashing = sun.misc.VM.isBooted() &&
                (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        init();
    }

源码解析

  • 前面的三个if判断主要是对输入的数据进行校验。
  • 然后进入正题,分析一下最核心的代码。
        int capacity = 1;
		//一只循环,知道capacity 大于你输出的初始长度为止
        while (capacity < initialCapacity)
            capacity <<= 1;
		//将加载因子赋值给该变量
        this.loadFactor = loadFactor;
        //取初始长度乘以加载因子(16*0.75)和MAXIMUM_CAPACITY+1的最小值,(其实就是临界吞吐量)赋值给threshold
        //那么MAXIMUM_CAPACITY是多少呢?:其实就是最大容量,详细请看下一块代码块
        threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        //实例化一个Entry长度的数组
        //注意:该长度并不是你传入的长度,而是比你传入长度大的最小的2的倍数。
        table = new Entry[capacity];
	//最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30;

上面的代码,如果读者看不懂,不要着急,我在和大家一起从头梳理一下。

  • 第一步:会对传入进来的数据进行校验,不符合规范,直接抛异常——代码块0
  • 第二步:一直进行左移运算 (左移一位扩大2倍),然后直到大于你传入的初始长度位置简单点说,就是大于等于传入初始长度且是2的整数倍。——代码块1
  • 第三步:求得该HashMap的临界吞吐量=(初始长度*默认加载因子),和最大容量,取二者的最小值。——代码块2
  • 第四步:创建一个Entry类型的数组,初始化的长度即为:第二步处理结束后的长度,即为2的整数倍数。——代码块3

4.总结

  • 问题一:调用无参构造器,底层数组长度是多少?
    • 答案:底层数组的默认长度为16.
  • 问题二:调用一个参数的构造器,指定长度为15,底层数组长度是15吗?

    • 答案:不一定是15,因为会取大于等于你传入初始长度的最小的2的整数倍。
  • 扩展:

    • 什么是加载因子?
      • 因为HashMap在底层是 数组+链表 (Java7中),因为在很多情况下,数组上总有那么一个或两个位置上不会有元素,所以,经过大量测试,以及考虑数组的利用率以及链表的长度不能太长,所以大量的测试,取值范围应该是0.7~0.75之间,最后取得0.75.
    • 什么是临界值?
      • 临界值即为:加载因子(即上一个问题) * 底层数组的当前长度(注意:是当前长度,不是默认长度)。也是判断底层Entry类型的table数组是否需要扩容的重要标准。

三、put方法

1.put方法流程图:

下图put方法的整个流程图,如果有些迷茫,没关系,继续读下面的。。。。
在这里插入图片描述

接下来, 带着大家一起学习HashMap的底层源码。

public V put(K key, V value) {
		//代码块0开始========================================
	        if (key == null)
	            return putForNullKey(value);
            
        //代码块1开始========================================
       		int hash = hash(key);
       		
        //代码块2开始========================================
        	int i = indexFor(hash, table.length);
        	
        //代码块3开始========================================
	        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
	            Object k;
	            //代码块4开始========================================
		            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
		                V oldValue = e.value;
		                e.value = value;
		                e.recordAccess(this);
		                return oldValue;
		            }
	        }

		代码块5开始========================================
        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

源码分析:这段代码是HashMap的put(),为了方便阅读,笔者把代码块分成了6块:即代码块0代码块5

2.为什么HashMap可以添加null元素?(强烈建议从put方法开始阅读)

答案:因为代码块0部分判断要添加的key是否为null,如果为null,是什么样子呢?请看下一块代码。

    private V putForNullKey(V value) {
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(0, null, value, 0);
        return null;
    }

大意为:

  • 第一步:.循环判断每一个元素,直到table(也就是HashMap的底层存储的数组:也就是table中已经没有元素了
  • 第二步:判断取出的每一个元素的key值是否为null,
    • 如果为空,就继续到下一个,继续判断。
    • 如果不为空,则就将旧的value值取出,把新的value值赋值给旧的value值,然后返回旧的value值,
  • 3.如果遍历所有的元素,依然没有找到key为null的元素,那么就调用addEntry(0, null, value, 0);方法,将该元素添加到table中(添加部分的代码,在下面的扩容部分会讲解)

3.为什么HashMap中的元素一定要重写hashCode()和equals()?(强烈建议从put方法开始阅读)

答案:因为代码块1代码块.4分别调用了hashCode()equals()

下图为代码块1调用的方法

final int hash(Object k) {
        int h = 0;
        if (useAltHashing) {
            if (k instanceof String) {
                return sun.misc.Hashing.stringHash32((String) k);
            }
            h = hashSeed;
        }

        h ^= k.hashCode();

        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

大意为:

  • 第一步:.将调用传入参数 (即要添加元素的key)** 的hashCode(),计算哈希值,然后根据求得的哈希值,经过一些位运算,获得一个数据。
  • 第一步:将第一步计算的值底层数组table的长度作为参数调用indexFor方法——也就是代码块2开始的部分。

加油,HashMap的源码就要看完了。。。

4.要添加的元素,如何找到对应存储的位置?

    static int indexFor(int h, int length) {
        return h & (length-1);
    }

大意为:传入的形参h(即代码块1 的运算结果)和形参length(即底层数组table的长度 )进行按位与运算。

例如:初始化时,table的长度为16,所以形参h与16-1即15进行按位与,15的二进制表示是1111H,所以结果就是取形参h的后四位,进行返回。

5.为什么HashMap中key作为是否重复的标准?(强烈建议从put方法开始阅读)

答案:因为每次比较的时候,都是因为采用的元素的key进行比较。
那么,一起结合源码来看一下。 我把代码块3代码块4拿过来,咱们一起好好品品。

//代码块3开始========================================
	for (Entry<K,V> e = table[i]; e != null; e = e.next) {
		Object k;
		//代码块4开始========================================
			if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
				V oldValue = e.value;
		        e.value = value;
		        e.recordAccess(this);
		        return oldValue;
		}
	}

源码大意:

  • 第一步:遍历table中每一个元素,直到该元素的next的为null
  • 第二步:如果遍历的元素(记作元素A)与要添加的元素(记作元素B)构成:
    • 元素A的哈希值等于元素B的哈希值并且元素A的key值的地址值等于元素Bkey值的地址 或者元素A的key与元素B的key进行equals()返回true
      • 如果找到了,则返回旧的value值,将新的value值覆盖旧的value值
      • 如果遍历所有的元素后,都没有找到,则执行代码块5的部分。

五.HashMap的扩容

1.HashMap什么情况下开始扩容?

    void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }

源码大意:如果当前底层Entry类型的table数组中元素的个数大于临界值(即:数组的当前) 当前元素要添加的数组的位置上有元素,即开始扩容。

2.HashMap的库容规则是什么?

仅仅截取上面代码块中if中的部分代码。。。

resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);

  • 第一步:调用resize(int newCapacity)方法,且传入的newCapacity参数为当前数组长度的二倍
  • 第二步:然后重新计算数组中元素的位置**
    void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];
        boolean oldAltHashing = useAltHashing;
        useAltHashing |= sun.misc.VM.isBooted() &&
                (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        boolean rehash = oldAltHashing ^ useAltHashing;
        transfer(newTable, rehash);
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

源码大意:

  • 第一步:对传入进来的长度进行判断
    • 判断是否大于最大值
  • 第二步:实例化一个新的数组,并且数组的长度为第一步处理过的值
  • 第三步:笔者暂时也看不懂,不过也希望其他读者有了解的,给笔者讲解一下。
  • 第四步:新实例化的数组赋值给底层的table数组
  • 第五步:重新计算临界值:临界值={ (现在的数组长度*加载因子)MAXIMUM_CAPACITY + 1 取最小值}

六.如何体现HashMap的链式结构

相信大家都已经知道了HashMap(Java7)底层是由数组+链表,且新的next指针是上一个元素的地址值,那么接下来,就一起用源码解释一下真相。

    void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }

源码分析:

  • 第一步:取出添加前的元素。
  • 第二步:实例化一个Entry,赋值给table中第一步取出的哪个元素在数组中的位置,并且传入了四个参数 ,
    • 参数一:要添加元素的哈希值
    • 参数二: 要添加元素的key值
    • 参数三:要添加元素的value值
    • 参数四:第一步取出的元素的地址值。

接下来,咱们看一下构造器。咱们看构造器的最后一个形参是Entry<K,V> n,在构造器中,把形参n赋值给新实例化的Entry对象的next成员变量(也就是next成员变量指向第一步取出的元素)

        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }

七.总结

恭喜读者,成功阅读完笔者的精心编写的博客,相信到此你也彻底理解了Java7中HashMap的底层数组的原理。是不是想问Java7的我懂了,但是Java8的呢?他们有什么区别呢?别着急,点击下面的链接,后面的内容更加精彩。。。

八.jdk中采用数组+链表+红黑树的HashMap源码分析链接

【Java集合篇】jdk8的HashMap源码分析

九.【Java集合篇】对比JDK7和8深度剖析ArrayList(只要看,就能懂)

【Java集合篇】对比JDK7和8深度剖析ArrayList(只要看,就能懂)

感谢您的阅读。。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值