Java set接口之HashSet集合的學習


Set接口的簡單概述

java.util.set接口繼承自Collection接口,它與Collection接口中的方法基本一致,並沒有對 Collection接口進行功能上的擴充,只是比collection接口更加嚴格了。set接口中元素是無序的,並且都會以某種規則保證存入的元素不出現重復。

簡述其特點:

  1. 不允許存儲重復的元素
  2. 沒有索引,也沒有帶索引的方法,不能使用普通的for循環遍歷

Set接口有多個實現類,java.util.HashSet是其常用的子類

HashSet集合的介紹

  1. java.util.HashSet是set接口的一個實現類,它所存儲的元素是不可重復的,並且元素都是無序的(即存取順序不一致)。
  2. java.util.HashSet底層的實現其實是一個java.util.HashMap支持
  3. HashSet是根據對象的哈希值來確定元素在集合中的存儲位置的,因此具有良好的存取和查找性能。
  4. 保證元素唯一性的方式依賴於: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集合存儲數據的結構(哈希表)

什么是哈希表

  1. 哈希表(又稱散列表),它是根據關鍵碼值(Key - Value)而直接進行訪問的數據結構。
  2. 也就是說,哈希表是通過把關鍵碼值(Key)映射到表中一個位置來訪問記錄(Value),以加快查找的速度。這個映射函數叫做哈希函數(散列函數),存放記錄的數組叫做哈希表。
  3. 在JDK1.8之前,哈希表底層采用數組+鏈表實現,即使用鏈表處理沖突,同hash值的鏈表都存儲在一個鏈表里。
  4. 但是當位於一個桶中的元素較多,即hash值相等的元素較多時,通過key值依次查找的效率較低。
  5. 而JDK1.8中,哈希表存儲采用數組+鏈表+或數組+紅黑樹實現,當鏈表長度超過閾值(8)時,將鏈表轉換為紅黑樹,這樣大大減少了查找時間。

什么是鏈表

鏈表是一種物理存儲單元上非連續、非順序的存儲結構,數據元素的邏輯順序是通過鏈表中的指針鏈接次序實現的。鏈表由一系列結點(鏈表中每一個元素稱為結點)組成,結點可以在運行時動態生成。每個結點包括兩個部分:一個是存儲數據元素的數據域,另一個是存儲下一個結點地址的指針域。

什么是紅黑樹

紅黑樹(又稱對稱二叉B樹),是一種自平衡二叉查找樹,是在計算機科學中用到的一種數據結構,典型的用途是實現關聯數組

什么是關聯數組

是一種具有特殊索引方式的數組。不僅可以通過整數來索引它,還可以使用字符串或者其他類型的值(除了NULL)來索引它。

哈希值

  1. 哈希值是通過一定的哈希算法,將一段較長的數據映射為較短小的二進制數據,這段小數據就是大數據的哈希值。
  2. 特點:它是唯一的,一旦大數據發生了變化,哪怕是一個微小的變化,他的哈希值也會發生變化。
  3. 作用:主要用途是用於文件校驗或簽名。

在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

圖形理解

img

  1. 可以看到,數組的每個位置放到一定的值,每部分值都對應一個哈希值。

  2. 當值的個數超過8個的時候,采用數組+紅黑樹;當值的個數不到8個的時候,采用數組+鏈表

  3. 如:

    值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集合是空的

  1. hashSet.add(s1)的時候
    1. 第一步:add()方法首先會調用s1的hashCode()方法,計算字符串"abc"的哈希值,其哈希值是96354,
    2. 第二步:查找集合中哈希值是96354中的元素,沒有發現哈希值是96354的key
    3. 第三步:將s1存儲到集合hashSet中(於是集合hashSet中存在哈希值96354,且對應這數據s1)
  2. hashSet.add(s2)的時候
    1. 第一步:add()方法首先會調用s2的hashCode()方法,計算字符串"abc"的哈希值,其哈希值是96354,
    2. 第二步:查找集合hashSet中是否存在哈希值是96354,即哈希值96354沖突,
    3. 第三步:s2調用equals()方法,和集合中哈希值是96354對應的元素進行比較
    4. 第四步:s2.equals(s1)返回true,即哈希值是96354對應的元素已經存在,所以就不添加s2進集合了(其中:s1 = "abc",s2 = "abc")
  3. hashSet.add("一號")的時候
    1. 第一步:調用 "一號" 的hashCode()方法,計算字符串 "一號" 的哈希值,其哈希值是640503,
    2. 第二步:查找集合中哈希值是640503中的元素,沒有發現哈希值是640503的key,
    3. 第三步:將 "一號" 存儲到集合hashSet中(於是集合hashSet中存在哈希值640503,且對應這數據 "一號")
  4. hashSet.add("二號")的時候
    1. 第一步:調用 "二號" 的hashCode()方法,計算字符串 "二號" 的哈希值,其哈希值是644843,
    2. 第二步:查找集合中哈希值是644843中的元素,沒有發現哈希值是644843的key,
    3. 第三步:將 "二號" 存儲到集合hashSet中(於是集合hashSet中存在哈希值644843,且對應這數據 "二號")
  5. 添加完成,集合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規范會拋出警告

img

測試一下會出現什么情況

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()方法。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM