撸啊撸,再次撸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了,这样可以减少数组的扩容次数,从而提高效率。

谢谢点赞

  • 创作不易, 非常欢迎大家的点赞、评论和关注(^_−)☆

  • 你的点赞、评论以及关注是对我最大的支持和鼓励,而你的支持和鼓励

  • 我继续创作高质量博客的动力 !!!

本文使用 mdnice
排版

原文 

https://juejin.im/post/5f05784a5188252e4d6409ae

本站部分文章源于互联网,本着传播知识、有益学习和研究的目的进行的转载,为网友免费提供。如有著作权人或出版方提出异议,本站将立即删除。如果您对文章转载有任何疑问请告之我们,以便我们及时纠正。

PS:推荐一个微信公众号: askHarries 或者qq群:474807195,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

转载请注明原文出处:Harries Blog™ » 撸啊撸,再次撸HashMap源码,踩坑源码中构造方法

赞 (0)
分享到:更多 ()

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址