文章目录
前言
HashSet与HashMap的扩容机制一直都是听别人说的,一问很多人又讲不明白,要么就是说看源码,要是小白都能看得懂源码还要大佬干嘛。一直想直观的看一下HashSet的扩容瞬间,但是又涉及反射啥啥的,很麻烦。但是最近我突然发现了一个小技巧,现在就带大家一同直观的看一下,HashSet到底是怎么扩容的。
提示:以下实验仅限IDEA
一、浅提一嘴HashSet的底层实现
HashSet的底层是基于HashTable实现的。HashTable在jdk8以前,是数组+链表;jdk8以后是数组+链表+红黑树
二、HashTable的扩容机制是什么?
相信大家都知道HashSet的扩容机制是什么,就算不知道,看完本文也知道了~
HashSet在初始创建时,会在底层创建一个初始容量为0的HashSet集合(其实是HashMap集合,只是不展示Value值而已)
当加入第一个元素时,HashTable会进行第一次扩容,数组长度变为16。学过ArrayList的小伙伴都知道,数组满了就扩容对不对~但是在HashTable这里不一样,HashTable有一个扩容因子0.75,意思就是说,当HashTable的数组容量到达最大程度的75%时就会触发扩容,每次扩容,容量都会扩大为原来的两倍。接下来我以HashSet为案例,就带大家直观的了解一下HashTable的扩容机制。
三、数组扩容的实验
1.实验前,要先设置一下IDEA
之所以设置这个,是因为这样在等下的Debug调试的时候就能直观的看到HashSet的容量变化了~
设置好以后,就可以开始实验了
首先创建一个HashSet集合,并且准备往里面添很多元素,但是我们在一开始的时候就打好断点。
代码如下(示例):
import java.util.HashSet;
public class Test {
public static void main(String[] args) {
HashSet<Integer> set = new HashSet<>();
int i = 0;
while (i < 2000) {
i += 1;
set.add(i);
}
System.out.println(set.size());
}
}
断点就打在while循环这里,如下图所示:
接下来用Debug运行,我们观察调试台的内容
运行后控制台是这样的,把set旁边的小箭头点开
我们主要观察 size 和 threshold 两个变量的动态变化。size就是数组的大小,threshold就是容量阈值,就是数组最大容量乘以下面的loadFactor(扩容因子)
到这里我们就证实了初始创建HashSet时,容量和长度都为0。
接下来我们就开始试验下一步:
2.添加第一条数据
Debug执行到当前添加元素后,再次点下一步就会发现神奇的现象:
size就是数组长度,只添加了一条数据,长度自然为1,但是threshold,也就是前面提到的阈值,等于12就证实了之前所说的,当添加了第一条数据后,数组就会进行第一次扩容,扩容的大小就是16,但是扩容的临界点,也就是阈值,阈值就等于16*0.75。
接下来小伙伴再点击下一步,直到长度为12时,添加第13条数据后,阈值就会再次发生变化,扩大到24,阈值到24就说明当前数组的长度为32,也证实了最开头说的,每次扩容为原来的两倍。
三、链表扩容的实验
记不记得前文有提到,HashSet的底层是由数组+链表+红黑树组成?其实最开始我混乱的地方就在于链表中的元素到底占不占数组的位置。到底是数组中的元素达到12个就扩容,还是整个HashSet中数组加链表的元素总和加起来是12个才扩容。
没错,现在就专门实验,只在链表中添加数据,再来看看容量的变化。
这里单单用整数类型已经不能满足要求了。我们改一下代码:
第1步
添加一个Animal类,类里面就设置一个名字、一个年龄,再用快捷键给它安排一个满参构造器构造器、get方法以及重写hashCode、equals方法。
注意:这里我重写了hashCode方法,其返回值永远都是0,底层判断的时候,由于hashCode值相同,所有元素都会添加到同一个位置上,jdk8以后新数据就会挂在原数据的后面,像链条一样。
代码如下(示例):
public class Animal {
private String name;
private int age;
public String getName() {
return name;
}
public int getAge() {
return age;
}
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Animal)) return false;
Animal animal = (Animal) o;
if (getAge() != animal.getAge()) return false;
return getName() != null ? getName().equals(animal.getName()) : animal.getName() == null;
}
@Override
public int hashCode() {
return 0;
}
}
第2步
改写上面的测试代码
代码如下(示例):
public class Test {
public static void main(String[] args) {
HashSet<Animal> animals = new HashSet<>();
int i = 0;
while (i < 2000) {
i += 1;
animals.add(new Animal("猫", i));
}
System.out.println(animals.size());
}
}
还是一样的,断点打在while,接下来就开始Debug~
开始还是一样的,但是当size等于8的时候,注意啦!!!当链表长度等于8时,
添加第9条数据,数组就会扩容了,扩容为原来的两倍!!!
诶???不是说元素达到12才会扩容吗?现在怎么8就扩容了。。。查了很多资料都没看到能说清楚的,源码也看不明白,,这个点就当知识点记下来叭,求评论区大神指导
继续实验的小伙伴就能发现,链表中其实不光8是扩容点,9也是扩容点,当添加到第10条元素的时候,又会扩容一次,阈值会扩容至48,也就是数组长度扩容至64了。但是下一次扩容也要到48个元素才会继续扩容了。
四、补充说明
当数组容量达到64并且单条链表长度大于8时,HashSet就会变成红黑树结构~
总结
充电时刻
不论是HashSet,还是HashMap,他们的扩容机制都是:
数组元素+链表元素的个数总和达到阈值就会扩容,但也不是绝对噢,当链表长度达到8、9时,不论数组中有无元素,不论总个数是否达到阈值,都会直接扩容!至于为什么链表会在长度为8、9时扩容还请评论区大佬指点一二~
我看到很多文章和博主这个点的概念都说的很模糊