撸啊撸,再次撸HashMap源码,踩坑源码中构造方法!!!每次都有收获

前言

点赞在看,养成习惯。

点赞收藏,人生辉煌。

点击关注【微信搜索公众号:编程背锅侠】,第一时间获得最新文章。

HashMap系列文章

第一篇 HashMap源码中的成员变量你还不懂? 来来来!!!整理好的成员变量源码解析

第二篇 撸啊撸,再次撸HashMap源码,踩坑源码中构造方法!!!每次都有收获

构造方法

构造一个空的 HashMap ,默认初始容量(16)和默认负载因⼦(0.75)

源码解析
// 构造一个无参数的构造方法
public HashMap() {
  // 将默认的加载因子0.75赋值给loadFactor,并没有创建数组
	this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
案例演示
@Test
public void test_hash_map_con_no(){
	HashMap<Integer, String> map = new HashMap<>();
}

构造⼀个具有指定的初始容量和默认负载因子(0.75)HashMap

源码解析
// 构造一个指定容量⼤⼩的构造函数
public HashMap(int initialCapacity) {
	this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
案例演示
@Test
public void test_hash_map_con_in(){
	HashMap<Integer, String> map = new HashMap<>(16);
}

构造一个具有指定的初始容量和负载因⼦的HashMap

源码解析
// 指定容量⼤小和加载因⼦的构造函数 initialCapacity: 指定的容量 loadFactor:指定的加载因⼦
public HashMap(int initialCapacity, float loadFactor) {
  // 判断初始化容量initialCapacity是否小于0
	if (initialCapacity < 0)
    // 如果⼩于0,则抛出非法的参数异常IllegalArgumentException
		throw new IllegalArgumentException("Illegal initial capacity: " +
				initialCapacity);
  // 判断初始化容量initialCapacity是否⼤于集合的最大容量MAXIMUM_CAPACITY->2的30次幂
	if (initialCapacity > MAXIMUM_CAPACITY)
    // 如果超过MAXIMUM_CAPACITY,会将MAXIMUM_CAPACITY赋值给initialCapacity
		initialCapacity = MAXIMUM_CAPACITY;
  // 判断负载因⼦loadFactor是否小于等于0或者是否是⼀个⾮数值
	if (loadFactor <= 0 || Float.isNaN(loadFactor))
    // 如果满足上述其中之一,则抛出非法的参数异常IllegalArgumentException
		throw new IllegalArgumentException("Illegal load factor: " +
				loadFactor);
  // 将指定的加载因⼦赋值给HashMap成员变量的负载因子loadFactor
	this.loadFactor = loadFactor;
  // tableSizeFor(initialCapacity) 判断指定的初始化容量是否是2的n次幂,如果不是那么会变为⽐指定初始化容量大的最小的2的n次幂。
	this.threshold = tableSizeFor(initialCapacity);
}
案例演示
@Test
public void test_hash_map_con_in_lo(){
  // 自定义初始化容量和加载因子
	HashMap<Integer, String> map = new HashMap<>(16, 0.75f);
}
疑惑代码
this.threshold = tableSizeFor(initialCapacity);
以上代码是源码中的最后一句代码,为啥不是this.threshold = tableSizeFor(initialCapacity) * this.loadFactor ?
  
这样才符合threshold的意思(当HashMap的size到达threshold这个阈值时会扩容)。 
但是,请注意,在jdk8以后的构造方法中,并没有对table这个成员变量进行初始化,table的初始化被推迟到了put方法中,在put⽅法中会对threshold重新计算,put⽅法的【resize()方法】具体实现我会在这个系列其他文章会进行讲解。
总结
如果这个构造函数的initialCapacity小于0,将会抛出非法异常IllegalArgumentException。
如果loadFactor的值是isNaN,则会抛出非法异常IllegalArgumentException。

构造一个包含另一个Map的构造函数和默认负载因子(0.75)

源码解析
public HashMap(Map<? extends K, ? extends V> m) {
	this.loadFactor = DEFAULT_LOAD_FACTOR;
  // 负载因子loadFactor变为默认的负载因子0.75
	putMapEntries(m, false);
}
案例演示
@Test
public void test_hash_map_con_map(){
	HashMap<Integer, String> map = new HashMap<>();
	map.put(1, "aaa");
	map.put(2, "bbb");
	map.put(3, "ccc");
  // 使用构造方法
	HashMap<Integer, String> all = new HashMap<>(map);
  // 遍历查看
	all.forEach((k, v) -> System.out.println("k: " + k + "    v: " + v));
}

tableSizeFor方法,返回比指定初始化容量大的最小的2的n次幂

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;
}
测试案例
// 搞个测试类【模拟上面的源码】 n >>> 1右移1位 |=按位运算
@Test
public void test_wei(){
    int n = 14 - 1; // 13
    n |= n >>> 1; // 15
    n |= n >>> 2; // 15
    n |= n >>> 4; // 15
    n |= n >>> 8; // 15
    n |= n >>> 16;// 15
    int i = (n < 0) ? 1 : (n >= 128) ? 128 : n + 1; // 16
    System.out.println(i);// 16
}
符号解析

>>>是右移符号。比如给定的值为5【0101】,右移一位为2【0010】。

|符号为或运算。比如给定的值11 | 15,11对应的二进制【1011】,15对应的二进制【1111】,11 | 15结果为【1111】15。

源码解析

当在实例化HashMap实例时,如果给定了initialCapacity(假设是5),由于HashMap的 capacity必须都是2的幂,因此这个方法用于找到大于等于initialCapacity(假设是5)的最小的2的幂。

initialCapacity如果就是2的幂,则返回的还是这个数)。

为什么要对cap做减1操作【int n = cap - 1】?

这是为了防⽌,如果cap已经是2的幂, ⼜没有执行这个减1操作,则执行完后面的几条无符号右移操作之后,返回的capacity将是这个cap的2倍。假如cap的值为8,经过上面的计算得到的还是8。

计算举例
以方法tableSizeFor(int cap)举例测试的数 cap = 65
        int n = cap - 1; ===>>>>  n = 65 - 1 = 64
        64 对应二进制 0100 0000 
        n >>> 1       
            右移10100 0000 ===>>>> 0010 0000
            n |= n >>> 1 对应于 0100 0000 | 0010 0000 = 0110 000096】

        n >>> 2       
            右移20110 0000 ===>>>> 0001 1000 
            n |= n >>> 2 对应于 0110 0000 | 0001 1000 = 0111 1000120】

        n >>> 4       
            右移40111 1000 ===>>>> 0000 0111
        n |= n >>> 4 对应于 0111 1000 | 0000 0111 = 0111 1111127】

        n >>> 8
            右移80111 1111 ===>>>> 0000 0000
            n |= n >>> 8 对应于 0111 1111 | 0000 0000 = 0111 1111127】

        n >>> 16
            右移160111 1111 ===>>>> 0000 0000
            n |= n >>> 16 对应于 0111 1111 | 0000 0000 = 0111 1111127】

        最后执行  return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; 返回12812827次幂,加一的原因是凑成整数次幂】 

putMapEntries添加键值对到集合中

// m:给定的集合。evict:最初构造此映射时为false。如果给定的集合为null,将会抛出空指针异常NullPointerException
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
  // 获取给定集合的长度
	int s = m.size();
  // 判断给定的集合长度是否大于0
	if (s > 0) {
    // 判断table是否已经初始化
		if (table == null) { // pre-size
      // 未初始化,s为m的实际元素个数。预先计算一个容量ft。这里为什么加1呢?有啥特殊的含义吗?
			float ft = ((float)s / loadFactor) + 1.0F;
      // 上面计算的容量不小于最大值将这个值赋值给t,否则赋值给最大值
			int t = ((ft < (float)MAXIMUM_CAPACITY) ?
					(int)ft : MAXIMUM_CAPACITY);
      // 判断这个容量是否大于0,大于就对这个容量进行格式化,格式为2的幂
			if (t > threshold)
				threshold = tableSizeFor(t);
		}
    // 之前的数组中有元素,判断参数中的数组长度是否大于数组容量
		else if (s > threshold)
      // 扩容
			resize();
    // 遍历给定的集合
		for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
      // 获取给定集合每个键值对的k和v
			K key = e.getKey();
			V value = e.getValue();
      // 将每一个entry的键值对放到数组中
			putVal(hash(key), key, value, false, evict);
		}
	}
}
float ft = ((float)s / loadFactor) + 1.0F这一行代码中为什么要加1.0F?
问题出现的源码
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY);
  • s/loadFactor的结果是⼩数,加1.0F(int)ft相当于是对小数做一个向上取整以尽可能的保证更大容量,更大的容量能够减少resize的调用次数。所以 + 1.0F是为了获取更大的容量。

  • 例如:原来集合的元素个数是6个,那么6/0.75是8,是2的n次幂,那么新的数组⼤小就是8了。

  • 然后原来数组的数据就会存储到长度是8的新的数组中了,这样会导致在存储元素的时候,容量不够,还得继续扩容,那么性能就会降低了,而如果+1呢,数组长度直接变为16了,这样可以减少数组的扩容次数,从而提高效率。

谢谢点赞

  • 创作不易, 非常欢迎大家的点赞、评论和关注(^_−)☆
  • 你的点赞、评论以及关注是对我最大的支持和鼓励,而你的支持和鼓励
  • 我继续创作高质量博客的动力 !!!
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

飞流2023

小主,破费了,回头请你喝咖啡!

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

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

打赏作者

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

抵扣说明:

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

余额充值