HashSet详解
HashSet的全面说明
-
HashSet 实现了Set接口
-
HashSet 实际上是HashMap
-
执行 Set set = new HashSet();
-
public HashSet() {
map = new HashMap<>();
}
-
可以存放null值,但是只能有一个null
-
HashSet不能保证元素的存取顺序一致
-
不能有重复的元素
-
HashSet线程不安全
-
没有带索引的方法,所以不能通过普通for循环进行遍历
HashSet的案例说明
案例1:
-
在执行add方法后,会返回一个boolean值
-
如果添加成功,返回true,否则返回false
-
可以通过 remove 指定删除哪个对象
public class Demo07 {
public static void main(String[] args) {
Set set = new HashSet();
System.out.println(set.add("join"));
System.out.println(set.add("lucy"));
System.out.println(set.add("join"));
System.out.println(set.add("jack"));
System.out.println(set.add("Rose"));
set.remove("join");
System.out.println(set);
}
}
案例2:
public class Demo08 {
public static void main(String[] args) {
HashSet hashSet = new HashSet();
hashSet.add("lucy");
hashSet.add("lucy");
hashSet.add(new Dog("tom"));
hashSet.add(new Dog("tom"));
System.out.println(hashSet);
}
}
class Dog {
private String name;
public Dog(String name) {
this.name = name;
}
@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
'}';
}
}
HashSet底层机制说明
-
HashSet 底层是HashMap,HashMap底层是(数组+链表+红黑树)
模拟简单的数组+链表结构
public class Demo01 {
public static void main(String[] args) {
Node[] table = new Node[16];
Node john = new Node("john", null);
table[2] = john;
Node jack = new Node("jack", null);
john.next = jack;
Node rose = new Node("Rose", null);
jack.next = rose;
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;
}
}
分析HashSet的添加元素底层是如何实现(hash()+equals())
-
HashSet 的底层是 HashMap
-
添加一个元素时,先得到hash值,会转成 -> 索引值
-
找到存储数据表table,看这个索引位置是否已经存放了元素
-
如果没有存放,则直接添加
-
如果存放了,调用 equals() 比较,如果相同,就放弃添加,如果不相同,则添加到最后
-
在java8中,如果一条 链表 的元素个数 大于 TREEIFY_THRESHOLD(默认是8)(链表准备添加第九个的时候) ,并且 table 的大小 >= MIN_TREEIFY_CAPACITY(默认是64),就会进行树化(红黑树)
public class Demo02 {
public static void main(String[] args) {
HashSet hashSet = new HashSet();
hashSet.add("java");
hashSet.add("php");
hashSet.add("java");
System.out.println("hashSet="+hashSet);
}
}
-
1.执行 HashSet hashSet = new HashSet();
public HashSet() {
map = new HashMap<>();
}
-
2.执行 hashSet.add("java");
public boolean add(E e) { //e = "java"
//PRESENT -> private static final Object PRESENT = new Object();
//PRESENT的目的是为了占位
return map.put(e, PRESENT)==null;
}
-
3.执行 return map.put(e, PRESENT)==null;
public V put(K key, V value) { //key = "java" value = PRESENT 共享
return putVal(hash(key), key, value, false, true);
}
-
4.先执行 hash(key)
-
得到对应的hash值
-
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
-
5.再执行 return putVal(hash(key), key, value, false, true);
-
transient Node<K,V>[] table; 和 Node<K,V>[] tab;
-
if ((tab = table) == null || (n = tab.length) == 0)
-
table和tab是引用赋值,所以两者的地址相同
-
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[]
//transient Node<K,V>[] table;
//if语句表示如果当前table是null或者tab的大小为0,则执行下面的语句
if ((tab = table) == null || (n = tab.length) == 0)
//执行 resize(),创建了大小为16的table数组
n = (tab = resize()).length;
//(1)根据key得到的hash值去计算该key应该存放到tab表的哪个索引位置
//并且把这个位置的对象,赋给p
//(2)判断p是否为null
//(2.1)如果p为null,表示还没有存放元素,就创建一个Node(key="java",value="PRESENT")
//(2.2)就放在该位置 tab[i] = newNode(hash, key, value, null);
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
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;
}
}
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;
}
-
6.执行 hashSet.add("php");
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
-
7.执行 return map.put(e, PRESENT)==null;
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
-
8.先执行 hash(key)
-
得到对应的hash值
-
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
-
9.再执行 return putVal(hash(key), key, value, false, true);
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;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
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;
}
}
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;
}
-
10.执行 hashSet.add("java");
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
-
11.执行 return map.put(e, PRESENT)==null;
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
-
12.先执行 hash(key)
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
-
13.再执行 return putVal(hash(key), key, value, false, true);
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;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//如果当前索引位置对应的链表的第一个元素的hash值和准备添加的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个节点,
// 如果到达了8个节点,就调用 treeifyBin(tab, hash); 如果满足
// if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) //64
// resize();,
// 则先对table扩容;如果该if语句不满足,则对当前这个链表进行树化(转成红黑树)
//(2)依次和该链表的每一个元素的比较过程中,如果有相同的情况,则直接break
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//TREEIFY_THRESHOLD = 8;
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();
afterNodeInsertion(evict);
return null;
}
分析HashSet的扩容和转成红黑树的机制
-
HashSet底层是HashMap,第一次添加时,table数组扩容到16,临界值(threshold)是 16*加载因子(loadFactor)是0.75 = 12
-
如果table数组使用到了临界值12,就会扩容到 16 * 2 = 32,新的临界值就是 32 * 0.75 = 24
-
在java8中,如果一条链表的元素个数到达 TREEIFY_THRESHOLD(默认是8),并且table的大小 >= MIN_TREEIFY_CAPACITY(默认是64),就会进行树化(转成红黑树),否则仍然采用数组扩容机制。
public class Demo04 {
public static void main(String[] args) {
HashSet hashSet = new HashSet();
for (int i = 1; i <= 12; i++) {
hashSet.add(new A(i));
}
System.out.println("hashSet="+hashSet);
}
}
class A {
private int n;
public A(int n) {
this.n = n;
}
@Override
public int hashCode() {
return 200;
}
}
-
执行 hashSet.add(new A(1)); 时,数组的大小扩容到16
-
执行 hashSet.add(new A(9)); 时,数组的大小扩容到32
-
执行 hashSet.add(new A(10)); 时,数组的大小扩容到64
-
执行 hashSet.add(new A(11)); 时,将该链表进行树化(转成红黑树)
public class Demo04 {
public static void main(String[] args) {
HashSet hashSet = new HashSet();
for (int i = 1; i <= 7; i++) {
hashSet.add(new A(i));
}
for (int i = 1; i <= 6; i++) {
hashSet.add(new B(i));
}
}
}
class A {
private int n;
public A(int n) {
this.n = n;
}
@Override
public int hashCode() {
return 200;
}
}
class B {
private int n;
public B(int n) {
this.n = n;
}
@Override
public int hashCode() {
return 100;
}
}
-
无论是在table数组上还是在链表上加入元素,size都会加1,size(元素的个数)的大小大于临界值后,就会对数组进行扩容。
if (++size > threshold)
resize();
-
执行到 hashSet.add(new A(1)); 时,数组的大小扩容到16
-
执行到 hashSet.add(new B(6)); 时,数组的大小扩容到32