HashSet
-
HashSet
底层是HashMap
-
HashSet
底层维护的是数组+单链表+红黑树 -
HashSet
在添加元素时,首先会根据当前元素的哈希值计算出一个对应的索引值 -
接下来根据计算出的索引值在数据表
table
中找到对应的位置并查看该位置是否已经存放了元素 -
如果该位置目前没有存放元素,则直接将当前元素存入该位置
-
如果该位置目前已经存放了元素,则调用该元素类的
equals()
方法逐一与当前位置的链表或红黑树中的元素依次比较 -
比较的过程中如果发现有与当前元素相同的元素,则放弃插入
-
如果全部比较完毕后没有发现与当前元素内容相同的元素,则插入当前元素
-
链表转红黑树的条件
(1)数据表
table
中某个位置后的链表中的元素个数大于等于TREEIFY_THRESHOLD
(默认值为8),table
位置上的元素也算一个(2)数据表table的长度大于等于
MIN_TREEIFY_CAPACITY
(默认值为64)
源码分析
public class Test {
public static void main(String[] args) {
HashSet<String> set = new HashSet<>();
set.add("Romeo");
set.add("Juliet");
set.add("Romeo");
}
}
// 调用HashSet的无参构造器
public HashSet() {
map = new HashMap<>(); // 底层调用HashMap的无参构造器
}
// HashMap中的重要属性
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka(also known as) 16
static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默认装填因子
static final int TREEIFY_THRESHOLD = 8; // 将链表转为红黑树的元素个数的临界值
static final int MIN_TREEIFY_CAPACITY = 64; // 将链表转为红黑树的散列表表长的最小值
transient Node<K,V>[] table; // 散列表
transient int size; // 散列表中所有元素的个数(包含链表和红黑树中的元素)
int threshold; // 散列表扩容的阈值(当前散列表的容量 * 散列表的装填因子)
final float loadFactor; // 散列表的装填因子
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // 使用DEFAULT_LOAD_FACTOR初始化散列表的装填因子
}
public boolean add(E e) { // e = "Romeo";
return map.put(e, PRESENT)==null;
// private static final Object PRESENT = new Object();
// value = PRESENT; 传入value只是为了占位(为什么要占位?)
// 猜测:该方法不是为add()方法量身定做的
}
public V put(K key, V value) { // value始终是PRESENT
// key = "Romeo";
// value = PRESENT;
return putVal(hash(key), key, value, false, true);
}
// 该方法用于计算对象key对应的hash值
// 注意:得到的hash值只是为了方便计算索引值,它并不是key对象的哈希值,key对象的哈希值是通过key.hashCode()来获取的
static final int hash(Object key) { // key = "Romeo";
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
// 通过key的哈希值计算新的hash值,hash值为计算索引值做准备
// 使用这样的算法计算hash值的目的是尽可能在计算索引值时避免哈希冲突
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
// hash: 对象key新计算的hash值
// key = "Romeo";
// value = PRESENT;
Node<K,V>[] tab; Node<K,V> p; int n, i; // 辅助变量
// 第一次扩容
if ((tab = table) == null || (n = tab.length) == 0) // table是散列表
// 第一次向集合中添加元素时,散列表为空,进入当前if
// 当前n为未添加新元素之前散列表的长度
n = (tab = resize()).length; // 获取扩容后的散列表的长度,此时n为扩容后的散列表的长度
// 通过hash值计算索引值来确定当前元素key应该存放在散列表中的哪个位置
// int i = (n - 1) & hash; // 通过hash值计算散列表的索引值
// Node<K,V> p = tab[i]; // 获取散列表索引值处的元素
if ((p = tab[i = (n - 1) & hash]) == null) // 当前索引位置处没有元素
// 1.通过hash值计算索引值
// 2.用i记录索引值,并用p记录当前位置上的元素(如果有元素的话)
// 3.根据索引值查找散列表对应位置上是否有元素
// (1)如果索引处没有元素,执行if中的代码
// (2)如果索引处有元素,执行else中的代码
tab[i] = newNode(hash, key, value, null);
// 对应位置没有元素,则直接在当前位置插入元素key
// 此处传入hash和key的目的是为了方便让再次加入的元素与散列表中的元素进行对比
else { // 索引处有元素
Node<K,V> e; K k; // 开发技巧:在哪里需要辅助变量,就在哪里定义
// 对应位置的元素与当前元素是相同的元素或是内容完全相同的元素(不能加入集合的情况)
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 1.当前索引位置上的元素的hash值与准备添加的key的hash值相同
// 2.当前索引位置上的对象元素与传入的key是同一个对象
// 3.传入的key不为空,且传入的对象的内容完全相同
e = p; // 用e是否为空来判断是否添加
// 索引处后为红黑树
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // 使用putTreeVal()方法在p后的红黑树中继续判断是否有相同的元素或是内容完全相同的元素,如果没有则添加并返回null
// 索引处后为单链表
else {
for (int binCount = 0; ; ++binCount) { // binCount用于记录当前检索过的元素数量并通过binCount的值来判断是否需要将链表转为红黑树
// e指针后移,e指针一直在p指针之后的一个位置
if ((e = p.next) == null) { // p已到达链表尾
p.next = newNode(hash, key, value, null); // 在链表尾插入当前元素key
if (binCount >= TREEIFY_THRESHOLD - 1) // binCount的值大于等于7时,也就是该链表中的元素个数已达到8个,此时转为红黑树
treeifyBin(tab, hash); // 转红黑树
// 注意:转为红黑树的条件还有一个,那就是当前散列表的长度是否达到或超过最小树化容量64,aka,如果散列表的长度没达到64,则不会立刻树化,而是先扩容,接下来继续等待新元素插入时达到该条件后才会树化
break; // 插入完之后直接结束检索
}
// 判断链表中的元素与当前元素是相同的元素或是内容完全相同的元素,该比较逻辑与之前的比较逻辑完全相同
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break; // 有相同的元素或是内容完全相同的元素直接结束检索
p = e; // p指针后移
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold) // 判断当前散集合中的元素个数是否已超过扩容阈值
resize(); // 超过则扩容
afterNodeInsertion(evict); // 该方法是留给HashMap的子类(如LinkedHashMap)实现的,对于HashMap来说,该方法不做任何操作,可以忽略
return null; // 返回空表示添加成功
}
扩容机制
- 第一次添加元素时,散列表
table
表长扩容到16
,临界值为:默认装填因子 * 默认初始化容量 =12
- 当在散列表
table
中添加第13
个元素时,会将table
扩容到32
,新的临界值为:当前容量 * 装填因子 =24
- 扩容的过程依次类推
// 该方法的作用是对散列表进行扩容
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table; // 保存当前散列表
int oldCap = (oldTab == null) ? 0 : oldTab.length; // oldCap代表的是散列表的旧容量,对于从未扩容过的散列表,其oldCap为0
int oldThr = threshold; // 散列表旧的扩容临界值
int newCap, newThr = 0; // 辅助变量
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
// 从未扩容过的散列表进入else
newCap = DEFAULT_INITIAL_CAPACITY; // 将散列表的新容量置为默认初始化容量16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 计算散列表新的扩容临界值为:默认装填因子*默认初始化容量 aka 12,该临界值的作用类似于缓冲层
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr; // 将刚计算的扩容临界值赋给当前散列表的扩容临界值
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; // 创建新的散列表
table = newTab; // 将新的散列表传给属性table,完成散列表的扩容
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab; // 将扩容后的散列表返回
}
模拟扩容
import java.util.HashSet;
public class Test {
public static void main(String[] args) {
HashSet<Integer> set = new HashSet<>();
for (int i = 0; i < 100; i++) {
set.add(i);
}
System.out.println(set);
}
}
注意:元素个数size
达到扩容阈值threshold
就会扩容,而不是散列表被占用的数量达到扩容阈值threshold
才会扩容
import java.util.HashSet;
public class Test {
public static void main(String[] args) {
HashSet set = new HashSet();
for (int i = 1; i <= 7; i++) {
set.add(new A(i));
}
for (int i = 1; i <= 7; i++) {
set.add(new B(i));
}
}
}
class A {
private int i;
public A(int i) {
this.i = i;
}
@Override
public int hashCode() {
return 100;
}
}
class B {
private int i;
public B(int i) {
this.i = i;
}
@Override
public int hashCode() {
return 200;
}
}
树化的条件
- 散列表中单条链表中的元素个数到达
8
- 散列表的表长到达最小树化容量
64
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// 未达到最小树化容量64则先扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
模拟树化
import java.util.HashSet;
public class Test {
public static void main(String[] args) {
HashSet<Integer> set = new HashSet<>();
for (int i = 1; i <= 12; i++) {
set.add(i);
}
System.out.println(set);
}
}
class A {
private int i;
public A(int i) {
this.i = i;
}
@Override
public int hashCode() {
return 200;
}
}
强化练习
- 定义一个Employee类,其中包含private成员属性name,salary,birthday(MyDate类型)
- MyDate类中包含private成员属性year,month,day
- 创建4个Employee对象放入HashSet中
- 当两个对象的name和birthday分别相同时,被视为相同员工,此时不能添加到集合中
import java.util.HashSet;
import java.util.Objects;
public class Test {
public static void main(String[] args) {
HashSet<Employee> set = new HashSet<>();
set.add(new Employee("Romeo", 25510, 1998, 9, 15));
set.add(new Employee("Juliet", 22101, 2002, 4, 10));
set.add(new Employee("Romeo", 23250, 1998, 9, 15));
set.add(new Employee("Romeo", 22222, 1998, 9, 15));
System.out.println(set);
}
}
class Employee {
private String name;
private double salary;
private MyDate birthday;
public Employee(String name, double salary, int year, int month, int day) {
this.name = name;
this.salary = salary;
this.birthday = new MyDate(year, month, day);
}
@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", salary=" + salary +
", birthday=" + birthday +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Employee employee = (Employee) o;
return Objects.equals(name, employee.name) &&
Objects.equals(birthday, employee.birthday);
}
@Override
public int hashCode() {
return Objects.hash(name, birthday.hashCode());
}
}
class MyDate {
private int year;
private int month;
private int day;
public MyDate(int year, int month, int day) {
this.year = year;
this.month = month;
this.day = day;
}
@Override
public String toString() {
return "MyDate{" +
"year=" + year +
", month=" + month +
", day=" + day +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MyDate myDate = (MyDate) o;
return year == myDate.year &&
month == myDate.month &&
day == myDate.day;
}
@Override
public int hashCode() {
return Objects.hash(year, month, day);
}
}