由浅入深HashMap1.8源码分析

看过源码后总结自己对于HashMap1.8源码的一些知识点:

HashMap1.8的底层是数组+链表+红黑树,实际上1.8中转红黑树的概率是非常非常低的,后面会提到。

我们先来分析下底层的主数组,下面是我画的数组,一般要给定数组的长度,那么给定多少合适?一般我们创建数组的时候如果给定int类型,那么数组里每个都是int类型,给定double类型等其它类型也是如此,那么当前数组对应的类型是什么?里面每个对象存放的是啥?带着这些疑问我们往下看。

在这里插入图片描述

下面我们来看一串代码

public class Test {
    //这是main方法的程序入口
    public static void main(String[] args) {            
        //定义HashMap集合
        HashMap<String,Integer> map = new HashMap<>();//数组+链表+红黑树
        //集合中添加元素:
        System.out.println(map.put("通话", 10));
        System.out.println(map.put("随便", 20));
        System.out.println(map.put("通话", 30));
        System.out.println(map.put("重地", 40));
        System.out.println(map.size());
        System.out.println(map);

        System.out.println("通话".hashCode());
        System.out.println("重地".hashCode());
    }
}

上面我存的第一个数据是(“通话”,10),这个数据是如何存进去的?大致思路是这样:首先把(“通话”,10)封装成一个对象,其次调用它的哈希值(有一点hashmap基础的应该是能理解的)。接着把(“随便”, 20)放进去,把(“通话”, 30)放进去,发现已经有了(“通话”,10),只能把10替换成30,接着把(“重地”, 40)放进去,这里我分别输出了"通话"对象和"重地"对象对应的哈希码,发现是一样的。
在这里插入图片描述
这两个对象的哈希码一样,说明发生了哈希碰撞,实际上刚才(“通话”, 30)把(“通话”, 10)给撞了一下,现在把"重地"和"通话"再撞一下,此时"重地"对象会以链表的形式往外追加。有一句话想必都听过”前7后8“,7与8相差是很大的,当然8更难。
在这里插入图片描述
在很多面试中,许多人总是会回答当链表>8的时候会转成红黑树,其实是不对的,实际上还要满足另外一个条件 ——> 主数组长度>=64,这样链表才能转成红黑树。

经过上面一系列补充,可以开始阅读源码了,这里标好了序号,按顺序阅读即可(红黑树源码未贴出)

public class HashMap<K,V>
    extends AbstractMap<K,V> //【1】继承的AbstractMap中,已经实现了Map接口
	//【2】又实现了这个接口,多余,但是设计者觉得没有必要删除,就这么地了
    implements Map<K,V>, Cloneable, Serializable{  //【1.5】泛型这里说一下
		
		
	//【3】后续会用到的重要属性:先粘贴过来:
    static final int DEFAULT_INITIAL_CAPACITY = 16;//哈希表主数组的默认长度
	//定义了一个float类型的变量,以后作为:默认的装填因子,加载因子是表示Hsah表中元素的填满的程度
	//太大容易引起哈西冲突,太小容易浪费  0.75是经过大量运算后得到的最好值
	//这个值其实可以自己改,但是不建议改,因为这个0.75是大量运算得到的
	static final float DEFAULT_LOAD_FACTOR = 0.75f;
	transient Entry<K,V>[] table;//主数组,每个元素为Entry类型
	transient int size;
	int threshold;//数组扩容的界限值,门槛值   16*0.75=12 
	final float loadFactor;//用来接收装填因子的变量
	
	//【4】查看构造器:内部相当于:this(16,0.75f);调用了当前类中的带参构造器
	public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }
	//【5】本类中带参数构造器:--》作用给一些数值进行初始化的!
	public HashMap(int initialCapacity, float loadFactor) {

        //【6】给capacity赋值,capacity的值一定是 大于你传进来的initialCapacity 的 最小的 2的倍数
        int capacity = 1;
        while (capacity < initialCapacity)
            capacity <<= 1;

		//【7】给loadFactor赋值,将装填因子0.75赋值给loadFactor
        this.loadFactor = loadFactor;
		//【8】数组扩容的界限值,门槛值
        threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
		
		//【9】给table数组赋值,初始化数组长度为16
        table = new Entry[capacity];
		   
    }
	//【10】调用put方法:
	public V put(K key, V value) {
		//【11】对空值的判断
        if (key == null)
            return putForNullKey(value);
		//【12】调用hash方法,获取哈希码
        int hash = hash(key);
		//【14】得到key对应在数组中的位置
        int i = indexFor(hash, table.length);
		//【16】如果你放入的元素,在主数组那个位置上没有值,e==null  那么下面这个循环不走
		//当在同一个位置上放入元素的时候
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
			//哈希值一样  并且  equals相比一样   
			//(k = e.key) == key  如果是一个对象就不用比较equals了
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
		//【17】走addEntry添加这个节点的方法:
        addEntry(hash, key, value, i);
        return null;
    }
	
	//【13】hash方法返回这个key对应的哈希值,内部进行二次散列,为了尽量保证不同的key得到不同的哈希码!
	final int hash(Object k) {
        int h = 0;
        if (useAltHashing) {
            if (k instanceof String) {
                return sun.misc.Hashing.stringHash32((String) k);
            }
            h = hashSeed;
        }
		//k.hashCode()函数调用的是key键值类型自带的哈希函数,
		//由于不同的对象其hashCode()有可能相同,所以需对hashCode()再次哈希,以降低相同率。
        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 ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
	//【15】返回int类型数组的坐标
	static int indexFor(int h, int length) {
		//其实这个算法就是取模运算:h%length,取模效率不如位运算
        return h & (length-1);
    }
	//【18】调用addEntry
	void addEntry(int hash, K key, V value, int bucketIndex) {
		//【25】size的大小  大于 16*0.75=12的时候,比如你放入的是第13个,这第13个你打算放在没有元素的位置上的时候
        if ((size >= threshold) && (null != table[bucketIndex])) {
			//【26】主数组扩容为2倍
            resize(2 * table.length);
			//【30】重新调整当前元素的hash码
            hash = (null != key) ? hash(key) : 0;
			//【31】重新计算元素位置
            bucketIndex = indexFor(hash, table.length);
        }
		//【19】将hash,key,value,bucketIndex位置  封装为一个Entry对象:
        createEntry(hash, key, value, bucketIndex);
    }
	//【20】
	void createEntry(int hash, K key, V value, int bucketIndex) {
		//【21】获取bucketIndex位置上的元素给e
        Entry<K,V> e = table[bucketIndex];
		//【22】然后将hash, key, value封装为一个对象,然后将下一个元素的指向为e (链表的头插法)
		//【23】将新的Entry放在table[bucketIndex]的位置上
        table[bucketIndex] = new Entry<>(hash, key, value, e);
		//【24】集合中加入一个元素 size+1
        size++;
    }
    //【27】
	void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
		//【28】创建长度为newCapacity的数组
        Entry[] newTable = new Entry[newCapacity];
        boolean oldAltHashing = useAltHashing;
        useAltHashing |= sun.misc.VM.isBooted() &&
                (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        boolean rehash = oldAltHashing ^ useAltHashing;
		//【28.5】转让方法:将老数组中的东西都重新放入新数组中
        transfer(newTable, rehash);
		//【29】老数组替换为新数组
        table = newTable;
		//【29.5】重新计算
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }
	//【28.6】
	void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
				//【28.7】将哈希值,和新的数组容量传进去,重新计算key在新数组中的位置
                int i = indexFor(e.hash, newCapacity);
				//【28.8】头插法
                e.next = newTable[i];//获取链表上元素给e.next
                newTable[i] = e;//然后将e放在i位置 
                e = next;//e再指向下一个节点继续遍历
            }
        }
    }
}
补充(两道经典面试题)

1.装填因子(负载因子或加载因子)为什么是0.75?

假若装填因子设置为1:空间利用率得到了很大的满足,很容易发生碰撞,产生链表–>查询效率低;
假若装填因子设置为0.5:碰撞的概率低,扩容1,产生链表的几率低,查询效率高,空间利用率太低;
因此取0.5-1的中间值:0.75,官方给的源码注释中也表明了:

// /* <p>As a general rule, the default load factor (.75) offers a good
//  * tradeoff between time and space costs. */

2.主数组的长度为什么必须为2^n?

原因1:h & (length-1)与h % length操作等效;等效的前提是:length必须为2^n,
原因2:防止哈希冲突,位置冲突
验证:

length:8
hash 3    0000 0011
length-1  0000 0111
------------------------
          0000 0011 -->3位置
hash 2    0000 0010
------------------------
          0000 0010 -->2位置

以上操作说明hash为3与hash为2算出来的位置不一样,前提是length必须为2^n
如果现在length取9

length:9
hash 3    0000 0011
length-1  0000 0111
------------------------
          0000 0000 -->0位置
hash 2    0000 0010
------------------------
          0000 0000 -->0位置

说明如果length不是2^n,
hash(3)和hash(2)计算出来的位置一样,位置冲突就会加链表,效率低,因此主数组长度为2^n可以防止位置冲突。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

༄༊心灵骇客༣

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值