前言
点赞在看,养成习惯。
点赞收藏,人生辉煌。
点击关注【微信搜索公众号:编程背锅侠】,第一时间获得最新文章。
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
右移1位 0100 0000 ===>>>> 0010 0000
n |= n >>> 1 对应于 0100 0000 | 0010 0000 = 0110 0000 【96】
n >>> 2
右移2位 0110 0000 ===>>>> 0001 1000
n |= n >>> 2 对应于 0110 0000 | 0001 1000 = 0111 1000 【120】
n >>> 4
右移4位 0111 1000 ===>>>> 0000 0111
n |= n >>> 4 对应于 0111 1000 | 0000 0111 = 0111 1111 【127】
n >>> 8
右移8位 0111 1111 ===>>>> 0000 0000
n |= n >>> 8 对应于 0111 1111 | 0000 0000 = 0111 1111 【127】
n >>> 16
右移16位 0111 1111 ===>>>> 0000 0000
n |= n >>> 16 对应于 0111 1111 | 0000 0000 = 0111 1111 【127】
最后执行 return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; 返回128【128为2的7次幂,加一的原因是凑成整数次幂】
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了,这样可以减少数组的扩容次数,从而提高效率。
谢谢点赞
- 创作不易, 非常欢迎大家的点赞、评论和关注(^_−)☆
- 你的点赞、评论以及关注是对我最大的支持和鼓励,而你的支持和鼓励
- 我继续创作高质量博客的动力 !!!