HashSet机制说明
基本说明
- HashSet实现类Set接口
- HashSet实际上是HashMap,源码:
public HashSet() {map = new HashMap<>();}
。 - HashMap底层是数组+链表+红黑树。
- 可以存放null值,但是只能存放一个null
- HashSet不保证元素的顺序,取决于hash后,再确定索引的结果
- 同Set接口一样不能有重复元素。
数组链表模拟
public class HashSetStructure {
// 模拟HashMap底层
public static void main(String[] args) {
//创建一个Node类型的数组(表)
Node[] table=new Node[16];// 断点1
System.out.println(table);
Node jack = new Node("jack", null);
table[2]=jack;// 断点2
Node rick = new Node("rick",null);
// 把rick这个节点挂载到jack
jack.next=rick;
Node rose=new Node("Rose",null);
// 把rose这个节点挂载到jack
rick.next=rose;
Node alicy=new Node("alicy",null);
// 把alicy放到table表中索引为3的位置。
table[3]=alicy;
System.out.println(table);
}
}
// 节点,存储数据,可以指向下一个节点,形成链表
class Node{
Object item;
Node next;
Node pre;
public Node(Object item, Node next) {
this.item = item;
this.next = next;
}
}
添加一个元素时,先获取hash值(hashCode方法),会转成索引值。
- 找到存储数据表table,看这个索引位置是否已存在元素
- 若无直接存入,若有则调用equals(该方法由程序员定义)比较,比较后相同放弃添加;不同则添加至最后
- 在jdk8中,若一条链表的元素个数到达TREEIFY_THRESHOLD(默认为8),且table的大小>=MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树)
源码解析
/*
// 1.
public HashSet() {
map = new HashMap<>();
}
*/
HashSet hashSet = new HashSet();
/*
//2. 执行add方法
public boolean add(E e) {
// PRESENT来自HashSet源码:private static final Object PRESENT = new Object();
return map.put(e, PRESENT)==null;
}
//3.执行put方法,执行hash(key)获得hash值:(h = key.hashCode()) ^ (h >>> 16)
public V put(K key, V value) { // key="hello" value=PRESENT(共享)
return putVal(hash(key), key, value, false, true);
}
//4.执行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的一个数组,Node[]类型
// if语句表示如果当前table是null或长度=0,则第一次扩容至16个空间
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 根据key得到的hash计算存放位置,同时把该位置的对象赋给p
// 判断p是否为null ,若p为null表示没有存放元素,就创建一个Node(key="hello",value=PRESENT)
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
// 辅助变量
Node<K,V> e; K k;
// 若当前索引位置对应链表的第一个元素和准备添加的key的hash值一样
// 并且满足:准备加入的key和p指向的Node节点的key是同一个对象 或 指向Node节点的key的equals()和准备加入的key比较后相同
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 判断是否为红黑树,是则调用putTreeVal进行添加
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 若table对应索引位置,
else {
// 已经是一个链表,进行for循环比较后都不相同(准备加入的元素不重复)
for (int binCount = 0; ; ++binCount) {
// 加入链表最后
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 在添加元素后立即判断该链表是否达到8个节点,达到调用treeifyBin() 转为红黑树
// 转为红黑树时进行判断,if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)resize();
// 成立则table扩容,否则转为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 比较元素相同(重复)直接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();
afterNodeInsertion(evict);
return null;
}
*/
hashSet.add("hello");
hashSet.add("world");
hashSet.add("hello");
System.out.println(hashSet);
HashSet扩容与转成红黑树
HashSet底层是HashMap,第一次添加时,table数组扩容到16,临界值(threshold)是16*加载因子,(locadFactor)是0.75=12
若table数组使用到12就会扩容为16*2=32,新的临界值就是32*0.75=24;以此类推。
HashSet hashSet = new HashSet();
for (int i = 0; i <= 100; i++) { //Debug可以查看扩容机制过程
hashSet.add(i);
}
在jdk8中,若一条链表的元素个数到达TREEIFY_THRESHOLD(默认为8),且table的大小>=MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树)
通过断点Debug可以观察到table的扩容过程与树化。
public class HashSetTest {
public static void main(String[] args) {
HashSet hashSet = new HashSet();
for (int i = 1; i < 12; i++) { // 断点
hashSet.add(new N(i));
}
System.out.println(hashSet);
}
}
class N{
private int id;
public N(int id) {
this.id = id;
}
@Override
// 重写返回的hash值改为固定
public int hashCode() {
return 999;
}
}
自定义比较通过idea快捷键【Alt】+【ins】选择equals() and hashcode()重写比较方式
自定义比较示例
public class HashSetTest {
public static void main(String[] args) {
HashSet hashSet = new HashSet();
hashSet.add(new Employee("jack",18));
hashSet.add(new Employee("jack",28));
hashSet.add(new Employee("jack",18));
hashSet.add(new Employee("rick",18));
System.out.println(hashSet);
}
}
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);
}
}