HashSet
的全面说明
继承关系图
构造器
Constructor and Description |
---|
HashSet() 构造一个新的空集合; 背景HashMap 实例具有默认初始容量(16)和负载因子(0.75)。 |
HashSet(Collection<? extends E> c) 构造一个包含指定集合中的元素的新集合。 |
HashSet(int initialCapacity) 构造一个新的空集合; 背景HashMap 实例具有指定的初始容量和默认负载因子(0.75)。 |
HashSet(int initialCapacity, float loadFactor) 构造一个新的空集合; 背景HashMap 实例具有指定的初始容量和指定的负载因子。 |
1.HashSet
实现了Set
接口
2.HashSet
实际上是HashMap
【由于HashSet
的构造器中创建了HashMap
对象】
public HashSet(){
map = new HashMap<>();
}
3.可以存放null
值,但是只能有一个null
4.HashSet
不保证元素是有序的,取决于hash
后,再确定索引的结果【即不保证存放元素的顺序和取出顺序一致】
5.不能有重复元素/对象。在Set
接口中已经体会
package collection_.collectionP.set_;
import java.util.HashSet;
/**
* @author: 海康
* @version: 1.0
*/
public class HashSet01 {
public static void main(String[] args) {
HashSet hashSet = new HashSet();
// 1.add方法返回的是一个布尔值:成功返回true 否则返回false
// 2.可以加入null值,但是只能添加一个
System.out.println(hashSet.add("湛江"));//true
System.out.println(hashSet.add("湛江"));//false
System.out.println(hashSet.add(null));//true
System.out.println(hashSet.add(null));//false
System.out.println(hashSet.add("海康"));//true
// 3.指定对象删除
boolean remove = hashSet.remove("海康");
// System.out.println(remove);
// 4.HashSet 不能添加相同的元素或对象
hashSet.add("lucy");// 可以添加
hashSet.add("lucy");// 不能添加因为以上语句指向常量池相同对象
hashSet.add(new Dog("tom"));// 可以不回成功
hashSet.add(new Dog("tom"));// 可以添加成功
// System.out.println("set"+hashSet);
// 在加深一下,非常经典的面试题
// 看源码分析
hashSet.add(new String("haikang"));// 可以添加成功
hashSet.add(new String("haikang"));// 不可以添加成功
System.out.println("hashSet"+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
底层是【数组+链表+红黑树】
下面的结论非常重要必须记住【背下】
- 先获取元素的哈希值【
hashCode
方法】 - 对象哈希值进行运算,得出一个索引值即为要存放在哈希表中的位置号
- 如果该位置上没有其他元素,则直接存放,如果该位置上已经有其他元素,则需要进行
equals
判断,如果相等,则不再添加。如果不相等,则以链表的方式添加
模拟数组+链表结构
package collection_.collectionP.set_;
/**
* @author: 海康
* @version: 1.0
*/
public class HashSetStructure01 {
public static void main(String[] args) {
// 模拟一个 HashSet的底层结构【其实就量模拟HashMap的底层结构】
// 1. 创建一个数组,数组的类型是 Node[]
// 2. 有些人直接把 Node[] 数组称为 表
Node[] table = new Node[16];
// 在索引为2的位置存放数据
Node haikaing = new Node("海康", null);
table[2] = haikaing;
Node jack = new Node("jack", null);
haikaing.next = jack;
Node rose = new Node("Rose", null);
jack.next = rose;
System.out.println(table[2]);
Node lucy = new Node("lucy", null);
table[3] = lucy;
}
}
class Node { // 结点 存储数据,可以指向下一个结点 ,从而形成链表
public Object item; // 存放数据
public Node next; // 指向下一个结点
public Node(Object item, Node next) {
this.item = item;
this.next = next;
}
@Override
public String toString() {
return "Node{" +
"item=" + item +
", next=" + next +
'}';
}
}
HashSet
底层剖析结论【非常重要面试题】
HashSet
底层是HasMap
- 添加一个元素时,先得到
hash
值 —> 转成 —> 索引值 - 找到存储数据表
table
,看看这个 索引位置是否已经存放有元素 - 如果没有直接加入
- 如果有调用
equals
比较,如果相同就放弃添加,如果不相同,则添加到最后 - 在
java8
中,如果一条链表的元素个数到达了TREEIFY_THRESHOLO
【默认是8】,并且table
的大小>=MIN_TREEIFY_CAPACITY
【默认64】,就会进行树化【红黑树】 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)
,就会进行树化【红黑树】,否则仍然采用数组扩容机制 - 扩容是以2倍的方式进行扩容的
注意是:每次向HashSet
集合中加入一个节点后,size
属性值就会加1,不管是加入是节点连接着节点还是直接加入到数组中,只要满足了到临界值,数组就会进行扩容
++modCount;
if (++size > threshold)// size是修改的次数【就是加入节点的次数】,threshold是临界值
resize();// 如果加入的次数大于临界值,则进行扩容
afterNodeInsertion(evict);
源码剖析:
public static void main(String[] args) {
HashSet<String> strings = new HashSet<>();
strings.add("湛江");
strings.add("海康");
strings.add("湛江");
for (int i = 0; i < 10; i++) {
strings.add("A_"+i);
}
}
步骤1:使用无参构造器:HashSet集合底层维护下面两个属性
private transient HashMap<E,Object> map;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();// 这个属性在添加时当占位符使用
下面是使用无参构造创建一个HashSet对象,其实底层在HashSet构造器中创建一个 HashMap集合赋给 map
public HashSet() {
map = new HashMap<>();
}
步骤2:添加方法
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
调用 HashMap 中 put 方法 并且 PRESENT 是一个占位符,是直接 new Object()对象赋给 PRESENT
步骤3:调用 HashMap 中 put 方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
调用 hash 方法 获取一个 哈希值
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
步骤4:真正添加方法
在 HashMap 类中 维护一个transient Node<K,V>[] table;属性第一添加时为null
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)判断当前表是否为空和表长度是否为0
如果是就调用 resize()方法进行扩容,就是创建一个数组
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指定向相同位置或equals值相同时 则不添加
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) {如果当前下一个位置为null时直接添加
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st、
判断一条链是否达到了8个,调用 treeifBin 方法,进行树化,注意同时还要table达到64
treeifyBin(tab, hash);
break;
}
如果两者相同则不添加
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
如果 key 相同时就替换值
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;
}
扩容算法
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
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
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;
下面是进行值的拷贝
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;
}
package com.hspedu.set_;
import java.util.HashSet;
@SuppressWarnings({"all"})
public class HashSetSource {
public static void main(String[] args) {
HashSet hashSet = new HashSet();
hashSet.add("java");//到此位置,第1次add分析完毕.
hashSet.add("php");//到此位置,第2次add分析完毕
hashSet.add("java");
System.out.println("set=" + hashSet);
/*
老韩对HashSet 的源码解读
1. 执行 HashSet()
public HashSet() {
map = new HashMap<>();
}
2. 执行 add()
public boolean add(E e) {//e = "java"
return map.put(e, PRESENT)==null;//(static) PRESENT = new Object();
}
3.执行 put() , 该方法会执行 hash(key) 得到key对应的hash值 算法h = key.hashCode()) ^ (h >>> 16)
public V put(K key, V value) {//key = "java" 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;
//(1)根据key,得到hash 去计算该key应该存放到table表的哪个索引位置
//并把这个位置的对象,赋给 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; //
//如果当前索引位置对应的链表的第一个元素和准备添加的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) 依次和该链表的每一个元素比较过程中,如果有相同情况,就直接break
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD(8) - 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;
//size 就是我们每加入一个结点Node(k,v,h,next), size++
if (++size > threshold)
resize();//扩容
afterNodeInsertion(evict);
return null;
}
*/
}
}
课堂练习1:
package collection_.collectionP.set_;
import java.util.HashSet;
import java.util.Objects;
/**
* @author: 海康
* @version: 1.0
*/
public class HashSetExercise01 {
public static void main(String[] args) {
/**
* 定义一个Employee类,该类包含:private成员属性name , age要求:
* 1.创建3个Employee放入HashSet中
* 2.当name 和 age 的相同时,认为是相同员工,不能添加到HashSet集合中
*/
HashSet hashSet = new HashSet();
hashSet.add(new Employee("海康",20));
hashSet.add(new Employee("海康",20));
hashSet.add(new Employee("湛江",22));
System.out.println(hashSet);
}
}
class Employee {
private String name;
private int age;
public Employee() {
}
public Employee(String name, int age) {
this.name = name;
this.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);
}
@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
package collection_.collectionP.set_;
import java.util.HashSet;
import java.util.Objects;
/**
* @author: 海康
* @version: 1.0
*/
public class HashSetExercise02 {
public static void main(String[] args) {
HashSet hashSet = new HashSet();
hashSet.add(new Employee01("海康",new MyDate(2000,11,11),86888.886));
hashSet.add(new Employee01("海康",new MyDate(2000,11,11),76888.886));
hashSet.add(new Employee01("湛江",new MyDate(2000,11,11),76888.886));
System.out.println(hashSet);
}
}
class Employee01 {
private String name;
private MyDate birthday;
private double salary;
public Employee01(String name, MyDate birthday, double salary) {
this.name = name;
this.birthday = birthday;
this.salary = salary;
}
@Override
public String toString() {
return "Employee01{" +
"name='" + name + '\'' +
", month=" + birthday +
", salary=" + salary +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Employee01 that = (Employee01) o;
return Objects.equals(name, that.name) && Objects.equals(birthday, that.birthday);
}
@Override
public int hashCode() {
return Objects.hash(name, birthday);
}
}
class MyDate {
private int year;
private int month;
private int 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);
}
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 +
'}';
}
}