HashSet全面说明
-
HashSet实现了Set接口
-
HashSet实际上是HashMap。
public HashSet() {
map = new Hashmap<>();
}
- 可以存放null值,但是只能有一个null。
- HashSet不保证元素是有序的,取决于 hash 后,再确定索引的结果。(即,不保证存放元素的顺序和取出顺序一致)
- 不能有重复的元素/对象。
案例一
public class HashSet01 {
public static void main(String[] args) {
HashSet set = new HashSet();
System.out.println(set.add("john")); //T
System.out.println(set.add("lucy")); //T
System.out.println(set.add("john")); //F
System.out.println(set.add("jack")); //T
System.out.println(set.add("Rose")); //T
set.remove("john");
System.out.println("set=" + set); //3个
}
}
===========输出结果===========
set=[Rose, lucy, jack]
//说明
//1. 在执行add方法后,会返回一个boolean值
//2. 如果添加成功,返回的是一个 true, 否则返回一个false
//3. 可以通过 remove 指定删除哪个对象
案例二
public class HashSet01 {
public static void main(String[] args) {
HashSet set = new HashSet();
//4. HashSet 不能存放相同的元素/数据
set.add("lucy"); //添加成功
set.add("lucy"); //加入不了,因为这里的 Lucy 都指向的是同一个常量池
set.add(new Dog("tom")); //添加成功
set.add(new Dog("tom")); //添加成功
System.out.println("set=" + set);
}
}
class Dog{//定义了一个Dog
private String name;
public Dog(String name) {
this.name = name;
}
@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
'}';
}
}
===========输出结果===========
set=[Dog{name='tom'}, Dog{name='tom'}, lucy]
HashSet底层机制说明
分析 HashSet 底层是 HashMap,HashMap 底层是(数组 + 链表 + 红黑树)
模拟一个HashSet的底层(HashMap 的底层结构)
public class HashSetStructure {
public static void main(String[] args) {
//1. 创建一个数组,数组的类型是 Node[]
//2. 有些人直接把这个 Node[] 数组称为 表
Node[] table = new Node[16];
System.out.println("table=" + table);
//3. 创建节点
Node john = new Node("john",null);
table[2] = john;
Node jack = new Node("jack",null);
john.next = jack; //将Jack 节点挂在到John
Node rose = new Node("rose", null);
jack.next = rose; //将rose 节点挂在到Jack
Node lucy = new Node("lucy", null);
table[3] = lucy;
System.out.println("table=" + table);
}
}
class Node{//节点,存储数据,可以指向下一个节点,从而形成链表
Object item; //存放数据
Node next; //指向下一个节点
public Node(Object item, Node next) {
this.item = item;
this.next = next;
}
}
在代码中打一个断点
执行后的结果,我们可以看到在索引为2和3的位置都挂上了节点。
添加元素源码分析
原理
步骤
- HashSet 底层是 HashMap。
- 添加一个元素时,先得到 hash 值 -> 会转成 -> 索引值。
- 找到存储数据表 table,看这个索引位置是否已经存放的有元素。
- 如果没有,直接加入。
- 如果有,调用 equals 方法比较,如果相同,就放弃添加,如果不相同,则添加到最后。
- 在 Java8 中,如果一条链表的元素个数达到 TREEIFY_THRESHOLD(默认是8),并且 table 的大小 >= MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树)。
public class HashSetSource {
public static void main(String[] args) {
HashSet hashSet = new HashSet();//断点
hashSet.add("java");
hashSet.add("php");
hashSet.add("java");
System.out.println("set=" + hashSet);
}
}
===========输出结果===========
set=[java, php]
源码解读
- 执行 HashSet 构造方法,获得一个 HashMap 的实例。
public HashSet() {
map = new HashMap<>();
}
- 执行 add() 方法,该方法返回了一个以 map 对象调用的方法 put(K key, V value) ,在这个方法中有两个参数:
- K Key:添加到集合中的数据(例如:hashSet.add(“java”))
- V value:等于 PRESENT: private static final Object PRESENT = new Object();
public boolean add(E e) {//e = "java"
return map.put(e, PRESENT)==null;
}
- 执行 put() 方法,该方法会执行 hash(key) 得到 key 对应的一个 hash 值 。计算 hash 值使用到的算法是:
(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);目的是为了让每一个 key 得到不同的 hash 值。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
- 执行 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;//定义了辅助变量。
//table 就是 HashMap 中的一个属性,类型是 Mode[]
//if语句表示如果当前 table 是 null,或者大小 = 0,就进行第一次扩容,到 16 个空间大小。
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//(1)根据 key 得到的 hash 值去计算该 key 应该存放到 table 表的哪一个索引位置,并且
//把这个位置的对象,赋值给辅助变量 p。
//(2)判断 p 是否为 null
//(2.1)如果 p 为 null,表示还没有存放过元素,就创建一个 Node
// newNode(hash, key, value, null) key, value两个值都是这一次操作传入的值。
if ((p = tab[i = (n - 1) & hash]) == null)//判断 tap[i] 的位置上有没有元素。
//(2.2)如果 p 不为 null,就将数据放在该位置
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//如果当前索引位置对应的链表的第一个元素和准备添加的 key 的 hash 值一样,并且满足下面的两个 //条件之一
//(1)准备加入的 key 和 p 指向的 Node 节点的 key 是同一个对象
//(2)p 指向的 Node 节点的 key 的 equals 方法和准备加入的 key 比较后相同
//就不能加入该元素
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//再判断 p 是不是一棵红黑树
//如果是,就调用 putTreeVal() 方法,来进行添加
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//如果 table 对应索引的位置,已经是一个链表,就使用 for 循环比较
//(1)依次和该链表的每一个元素比较后,都不相同,则加入到该链表的最后
// 注意:在把元素添加到链表后,立即判断该链表是否已经达到8个节点
// 就调用 treeifyBin() 对当前链表进行树化,转成红黑树
// 注意:在转成红黑树时还进行一个判断
// if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY(64))
// resize();
// 如果上面的条件成立,就先对 table 表扩容,
// 只有上面条件不成立时,才进行转换,变成红黑树
//(2)依次和该链表的每一个元素比较过程中,如果有相同的情况,就直接 breaker。
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;
}
}
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();
//HashMap 的一个空方法,方法体中什么都没有干,存在的目的是为了让 HashMap 的实现子类去实现。
afterNodeInsertion(evict);
return null;
}
/*
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }
*/
- 在 putVal() 方法中又执行了 resize() 方法,这个方法是 HashMap 中的核心,主要就是实现对集合的扩容。
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;//将 table(第一次添加数据table为 null) 赋值给新数组 oldTab。
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;//临界值(12)
int newCap, newThr = 0;
if (oldCap > 0) {//第一次添加数据时 oldCap 为 0,所以不会进入到这个if循环
//扩容
if (oldCap >= MAXIMUM_CAPACITY) {
//原数组长度大于最大容量(1073741824) 则将 threshold 设为 Integer.MAX_VALUE = 2147483647
//接近 MAXIMUM_CAPACITY 的两倍
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 新数组长度 是原来的2倍,
// 临界值也扩大为原来2倍
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
// 如果原来的 thredshold 大于 0 则将容量设为原来的 thredshold
// 在第一次带参数初始化时候会有这种情况
newCap = oldThr;
else { // zero initial threshold signifies using defaults
// 在默认无参数初始化会有这种情况
newCap = DEFAULT_INITIAL_CAPACITY;(16)
//当集合的容量使用达到了 newThr 时会进行扩容
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);(12)
}
if (newThr == 0) {
// 如果新的容量 == 0
// loadFactor 哈希加载因子 默认 0.75,可在初始化时传入,16*0.75=12 可以放12个键值对
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;
if (oldTab != null) {// 如果原来的table有数据,则将数据复制到新的table中
for (int j = 0; j < oldCap; ++j) {// 根据容量进行循环整个数组,将非空元素进行复制
Node<K,V> e;
if ((e = oldTab[j]) != null) {// 获取数组的第j个元素
oldTab[j] = null;
if (e.next == null)// 如果链表只有一个,则进行直接赋值
// e.hash & (newCap - 1) 确定元素存放位置
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;
}