Set接口的簡單概述
java.util.set接口繼承自Collection接口,它與Collection接口中的方法基本一致,並沒有對 Collection接口進行功能上的擴充,只是比collection接口更加嚴格了。set接口中元素是無序的,並且都會以某種規則保證存入的元素不出現重復。
簡述其特點:
- 不允許存儲重復的元素
- 沒有索引,也沒有帶索引的方法,不能使用普通的for循環遍歷
Set接口有多個實現類,java.util.HashSet是其常用的子類
HashSet集合的介紹
- java.util.HashSet是set接口的一個實現類,它所存儲的元素是不可重復的,並且元素都是無序的(即存取順序不一致)。
- java.util.HashSet底層的實現其實是一個java.util.HashMap支持
- HashSet是根據對象的哈希值來確定元素在集合中的存儲位置的,因此具有良好的存取和查找性能。
- 保證元素唯一性的方式依賴於:hashCode與 equals方法。
代碼簡單理解
import java.util.HashSet;
import java.util.Iterator;
public class DemoHashSet {
public static void main(String[] args) {
// 創建set集合(HashSet)
HashSet<String> hashSet = new HashSet<>();
// 使用add方法想HashSet集合中添加元素
hashSet.add("A");
hashSet.add("B");
hashSet.add("C");
hashSet.add("D");
hashSet.add("A");
System.out.println("集合中的元素:" + hashSet);
System.out.println("==============================");
// 使用迭代器遍歷集合
Iterator<String> ite = hashSet.iterator();
while (ite.hasNext()) {
System.out.println(ite.next());
}
System.out.println("==============================");
// 使用增強for循環遍歷集合(不能使用普通的for循環,對HashSet集合進行遍歷)
for (String s: hashSet) {
System.out.println(s);
}
}
}
輸出結果:
集合中的元素:[A, B, C, D]
==============================
A
B
C
D
==============================
A
B
C
D
注意:普通for循環不能遍歷HashSet集合,HashSet集合中沒有重復的元素,元素的存儲順序不一致。
HashSet集合存儲數據的結構(哈希表)
什么是哈希表
- 哈希表(又稱散列表),它是根據關鍵碼值(Key - Value)而直接進行訪問的數據結構。
- 也就是說,哈希表是通過把關鍵碼值(Key)映射到表中一個位置來訪問記錄(Value),以加快查找的速度。這個映射函數叫做哈希函數(散列函數),存放記錄的數組叫做哈希表。
- 在JDK1.8之前,哈希表底層采用數組+鏈表實現,即使用鏈表處理沖突,同hash值的鏈表都存儲在一個鏈表里。
- 但是當位於一個桶中的元素較多,即hash值相等的元素較多時,通過key值依次查找的效率較低。
- 而JDK1.8中,哈希表存儲采用數組+鏈表+或數組+紅黑樹實現,當鏈表長度超過閾值(8)時,將鏈表轉換為紅黑樹,這樣大大減少了查找時間。
什么是鏈表
鏈表是一種物理存儲單元上非連續、非順序的存儲結構,數據元素的邏輯順序是通過鏈表中的指針鏈接次序實現的。鏈表由一系列結點(鏈表中每一個元素稱為結點)組成,結點可以在運行時動態生成。每個結點包括兩個部分:一個是存儲數據元素的數據域,另一個是存儲下一個結點地址的指針域。
什么是紅黑樹
紅黑樹(又稱對稱二叉B樹),是一種自平衡二叉查找樹,是在計算機科學中用到的一種數據結構,典型的用途是實現關聯數組。
什么是關聯數組
是一種具有特殊索引方式的數組。不僅可以通過整數來索引它,還可以使用字符串或者其他類型的值(除了NULL)來索引它。
哈希值
- 哈希值是通過一定的哈希算法,將一段較長的數據映射為較短小的二進制數據,這段小數據就是大數據的哈希值。
- 特點:它是唯一的,一旦大數據發生了變化,哪怕是一個微小的變化,他的哈希值也會發生變化。
- 作用:主要用途是用於文件校驗或簽名。
在Java程序中的哈希值
/**
* 在Java中哈希值:是一個十進制的整數,由系統隨機給出的二進制數經過換算得到的
* 其實它就是對象的地址值,是一個邏輯地址,是模擬出來得到地址,並不是數據實際存儲的物理地址
*
* 在Object類有一個方法,可以獲取對象的哈希值:
* int hashCode() 返回對象的哈希碼值
* hashCode()方法源碼:
* public native int hashCode();
* native:代表該方法是調用本地操作系統的方法
*/
// 隨便創建一個類
public class Person extends Object{
private String name;
private int age;
public Person() {
}
public Person(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;
}
}
// 用這個類創建對象,查看對象的哈希值
public class DemoHashCode {
public static void main(String[] args) {
// 創建一個p1對象,查看其哈希值
Person p1 = new Person("LeeHua", 21);
// 調用Object的hashCode方法,獲取哈希值,p1的哈希值是不變的
int h1 = p1.hashCode();
System.out.println(h1);
// 創建一個p2對象,查看其哈希值
Person p2 = new Person("WanTao", 20);
// 調用Object的hashCode方法,獲取哈希值,p2的哈希值也是不變的
int h2 = p2.hashCode();
System.out.println(h2);
// 查看p1、p2的地址值
System.out.println(p1);
System.out.println(p2);
}
}
輸出結果:
1639705018
1627674070
view.study.demo18.Person@61bbe9ba
view.study.demo18.Person@610455d6
假如覆蓋重寫hashCode方法,所創建的對象的哈希值就會被影響
如:
public class Person1 extends Object{
/**
* 重寫hashCode方法
* @return 哈希值
*/
@Override
public int hashCode() {
return 666;
}
}
public class DemoHashCode1 {
public static void main(String[] args) {
// 創建一個p1對象,查看其哈希值
Person1 p1 = new Person1();
// 調用Object的hashCode方法,獲取哈希值,p1的哈希值是不變的
int h1 = p1.hashCode();
System.out.println(h1);
// 查看p1、p2的地址值
System.out.println(p1);
}
}
輸出結果:
666
view.study.demo18.Person1@29a
如:我們常用的String類,它也覆蓋重寫了hashCode方法
public class DemoStringHashCode {
public static void main(String[] args) {
/*
String類的哈希值
(String類重寫了Object類的hashCode方法)
*/
String s1 = new String("LeeHua");
String s2 = new String("WanTao");
System.out.println(s1.hashCode());
System.out.println(s2.hashCode());
}
}
輸出結果:
-2022794392
-1711288770
圖形理解
-
可以看到,數組的每個位置放到一定的值,每部分值都對應一個哈希值。
-
當值的個數超過8個的時候,采用數組+紅黑樹;當值的個數不到8個的時候,采用數組+鏈表
-
如:
值1、2、3、4的哈希值都是11
值13、14、15的哈希值都是15
值a、b、c、d、e、f、g的哈希值都是89
Set集合存儲元素不重復原理
set集合在調用add()方法的時候,add()方法會調用元素的hashCode()方法和 equals()方法判斷元素是否重復
代碼舉例
import java.util.HashSet;
public class DemoStringHashCode1 {
public static void main(String[] args) {
HashSet<String> hashSet = new HashSet<>();
String s1 = new String("abc");
String s2 = new String("abc");
hashSet.add(s1);
hashSet.add(s2);
hashSet.add("一號");
hashSet.add("二號");
System.out.println("s1的哈希值:" + s1.hashCode());
System.out.println("s2的哈希值:" + s2.hashCode());
System.out.println("一號的哈希值:" + "一號".hashCode());
System.out.println("二號的哈希值:" + "二號".hashCode());
System.out.println("HashSet集合:" + hashSet);
}
}
輸出結果:
s1的哈希值:96354
s2的哈希值:96354
一號的哈希值:640503
二號的哈希值:644843
HashSet集合:[二號, abc, 一號]
代碼講解
最初,hashSet集合是空的
- hashSet.add(s1)的時候
- 第一步:add()方法首先會調用s1的hashCode()方法,計算字符串"abc"的哈希值,其哈希值是96354,
- 第二步:查找集合中哈希值是96354中的元素,沒有發現哈希值是96354的key
- 第三步:將s1存儲到集合hashSet中(於是集合hashSet中存在哈希值96354,且對應這數據s1)
- hashSet.add(s2)的時候
- 第一步:add()方法首先會調用s2的hashCode()方法,計算字符串"abc"的哈希值,其哈希值是96354,
- 第二步:查找集合hashSet中是否存在哈希值是96354,即哈希值96354沖突,
- 第三步:s2調用equals()方法,和集合中哈希值是96354對應的元素進行比較
- 第四步:s2.equals(s1)返回true,即哈希值是96354對應的元素已經存在,所以就不添加s2進集合了(其中:s1 = "abc",s2 = "abc")
- hashSet.add("一號")的時候
- 第一步:調用 "一號" 的hashCode()方法,計算字符串 "一號" 的哈希值,其哈希值是640503,
- 第二步:查找集合中哈希值是640503中的元素,沒有發現哈希值是640503的key,
- 第三步:將 "一號" 存儲到集合hashSet中(於是集合hashSet中存在哈希值640503,且對應這數據 "一號")
- hashSet.add("二號")的時候
- 第一步:調用 "二號" 的hashCode()方法,計算字符串 "二號" 的哈希值,其哈希值是644843,
- 第二步:查找集合中哈希值是644843中的元素,沒有發現哈希值是644843的key,
- 第三步:將 "二號" 存儲到集合hashSet中(於是集合hashSet中存在哈希值644843,且對應這數據 "二號")
- 添加完成,集合hashSet = [abc, 一號, 二號]
HashSet存儲自定義類型元素
hashSet存儲自定義類型元素,那么自定義的類必須重寫hashCode()方法和equals()方法,否則添加的元素可以出現重復,我們平時使用的類型,它們都重寫類hashCode()方法和equals()方法。
假如不重寫hashCode()方法和equals()方法
例子:
// 隨便創建一個類,作為HashSet存入數據的類型
public class Person{
private String name;
private int age;
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
public Person() {
}
public Person(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;
}
}
IDEA 編譯,阿里巴巴Java Code規范會拋出警告
測試一下會出現什么情況
import java.util.HashSet;
public class Demo01PersonHashSet {
public static void main(String[] args) {
HashSet<Person> hashSet = new HashSet<>();
Person p1 = new Person("小明", 20);
Person p2 = new Person("小明", 20);
Person p3 = new Person("小紅", 20);
hashSet.add(p1);
hashSet.add(p2);
hashSet.add(p3);
System.out.println(hashSet);
}
}
輸出結果:
[Person{name='小明', age=20}, Person{name='小明', age=20}, Person{name='小紅', age=20}]
可以看到,hashSet集合里面可以存在重復的元素。
重寫hashCode()方法和equals()方法
還是上面這個例子:
在Person類里面添加要重寫hashCode()、equals()方法的代碼即可,要添加的代碼如下
public class Person{
@Override
public boolean equals(Object o) {
// 參數 == 對象
if (this == o) {
return true;
}
// 傳入參數為空,或者對象與參數的hashCode不相等
if (o == null || getClass() != o.getClass()) {
return false;
}
// 向下轉型,把Object類型轉型為Person類型
Person person = (Person) o;
// 返回 age,name
return age == person.age &&
Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
再次用上面的代碼測試一下Person類型的數據添加是否會出現重復:
輸出結果:
[Person{name='小明', age=20}, Person{name='小紅', age=20}]
可以看到,輸出結果中,hashSet集合的元素並沒有重復,因此,如果我們想要用HashSet集合存儲自定義類型的數據,一定要記得覆蓋重寫hashCode()方法和equals()方法。