文章目录
- 特点
- 结论
- 源码解读
- 构造器
- 添加元素
- 小结说明
- 练习(重要*掌握)
- 思考
特点
-
无序、无索引
-
不可重复(地址),可存一个null
-
不可用索引取出
-
存放和取出顺序不一定一样
-
但每次取出的顺序是一样的
-
遍历只能迭代器和增强for
-
底层其实是HashMap
结论
源码解读
构造器
HashSet hs = new HashSet();底层实现
可见底层HashSet底层就是HashMap;
添加元素
hs.add(“1”)底层实现
- 首先进入add方法
相当于map添加元素e就是我们存的值,PRESENT是底层统一提供的value,因为map是需要key-value的,但我们只需要使用这里的key,所以value我们不用管 - add方法调用put方法进行存值
我们知道,HashSet是无序的,底层的排列方式是按照Hash值进行排列的,所以先进入hash(key)进行Hash值的计算 - 进入hash(key)进行hash值的计算
这里就可以看到hash值的计算方法,如果传进来null的话,hash值直接为0,不为null的话,得到key的hashCode(hashCode是Object的方法,每个对象都会有自己的hashCode),再右移16位。 - 这是String类型的hashCode计算
我们自己写的类,hashCode就由我们自己定义 - 好hash值算好,我们回到第二步的putVal方法进行存值
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;//设置一些辅助变量
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
/*
1.table就是map的属性,是一个数组,即存的内容
2.(tab = table) == null || (n = tab.length) == 0
这就是先tab = table然后判断是否为空或者长度为0
3.如果表中没有数据,就resize()重新设置表的长度,见resize讲解
*/
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
/*
p = tab[i = (n - 1) & hash]:p指向tab数组将要保存的数据位置,
位置就是按照得到的hash值i = (n - 1) & hash再经过这样的计算
判断这个位置是否为null即这个位置是否保存了数据
如果没保存的话,就直接保存在这个位置
*/
else {
/*
否则就说明那个位置有数据了,就要考虑以下两点
1. 是否相同(和其中任意一个已有数据相同,就直接替换。注意这里的相同判断是equals方法,大部分equals方法是需要我们程序员自己定义的)
2. 往后添加是链型添加还是树形添加
hashmap底层添加原理
当该位置有数据,就以链表形式添加在最后
但要考虑,如果链表后缀到达8时,并且表长超过64,链表就变成红黑树的形式保存,如果如果链表后缀到达8时,并且表长没有超过64,则还是添加在最后,与此同时表长变成原来2倍。
*/
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//这里判断是否equals相同
e = p;
else if (p instanceof TreeNode)
//这里判断是树形添加,树形添加方式就不细说了,涉及到红黑树
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//这里判断是链型添加,这就是链表的添加,遍历比较
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//以上的三个判断,就是找到新数据需要添加的位置,如果找到位置应该是放在最后的,e就是null,因为最后就是空嘛,,如果找到了相同元素,这是e就不是空了,就走下面的个方法
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)//开头传入的参数flase,说明冲突时会进行替换操作
e.value = value;
afterNodeAccess(e);//子类实现,这里不实现
return oldValue;
}
}
++modCount;//改变次数记录
if (++size > threshold)//看当前size是否超过扩容边界,如果超过,就要进行resize
resize();
afterNodeInsertion(evict);//这个方法是空方法,我们不用管,这个是留给hashset子类需要的话去实现的
return null;
}
- 5.1 resize讲解
这里就不细讲了,可以参考别人写的https://juejin.cn/post/7035613144047157279
代码整体框架:
首先进行新的容量确定
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;//先把表表赋值给oldTab
int oldCap = (oldTab == null) ? 0 : oldTab.length;//判断原来的表是否为null并赋值
int oldThr = threshold;//原来扩容界限
/*hashmap并不是装满再扩容的,而是到达75%容量时,进行扩容,所以这里保存一下原来的扩容界限*/
int newCap, newThr = 0;//新的容量和扩容界限先定义为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
//当没有容量且,边界值>0 时,说明边界值初始化过,就将新表容量等于边界值
newCap = oldThr;
else { // zero initial threshold signifies using defaults
/*
一开始为null时,就走到这里进行初始化
DEFAULT_INITIAL_CAPACITY = 1 << 4即newCap一开始赋值为16
DEFAULT_LOAD_FACTOR = 0.75即扩容临界值为容量的0.75倍
*/
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//如果新的边界值,在上面没有设置过,就用这个方法设值
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) {//不为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;
}
小结说明
- 底层是hashmap
- 存入数据前先计算hash值,再存值
- 存值,需要考虑,该位置是否重复,是否按链表存还是树存
- 表长扩容是2倍
- 加载因子是0.75,当添加的数据大于12时(无论怎么添加),进行扩容
- 长度大于8时(且表长大于等于64时)转换为红黑树,小于6时退化为链表
练习(重要*掌握)
定义一个Employee类,该类包含:private成员属性name.age
要求:1.创建3个Employee对象放入HashSet中
2.当name和age的值相同时,认为是相同员工,不能添加到HashSet集合中之后写的时候会涉及到的
以上两个分别表示当name和age相同时,equals和hashcode返回true
import java.util.HashSet;
import java.util.Objects;
class Employee{
private String name;
private int age;
public Employee(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Employee employee = (Employee) o;
return age == employee.age && Objects.equals(name, employee.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
public class Test {
@SuppressWarnings({"all"})
public static void main(String[] args){
HashSet hs = new HashSet();
hs.add(new Employee("xiaoming",12));
hs.add(new Employee("wanggang",18));
hs.add(new Employee("xiaoming",12));
//如果不重写equals和hashcode的话,就会按照默认的equals和hashcode执行
//就会有存入三个值
//我们重写了之后,就会按照我们的给出equals和hashcode,这时,就会存入两个值
System.out.println(hs);
}
}
思考
定义一个Employee类,该类包含:private成员属性name,sal,birthday(MyDate类型),其中 birthday为 MyDate类型(属性包括:year, month, day),要求:
1.创建3个Employee放入 HashSet中
2.当name和birthday的值相同时,认为是相同员工,不能添加到HashSet集合中