1. 程式人生 > >Java中HashSet的重複性與判等運算過載

Java中HashSet的重複性與判等運算過載

目錄

  • 還有一個故事……(平行世界篇)
    • 還有一個美麗的夢幻家園:java.util
    • 並且還有一個善戰的達拉崩巴:HashSet
  • 還有另外一個故事(不是虛假傳說)
    • 還有一對塗滿毒藥的奪命雙匕:equals和hashCode
      • 但是為什麼這次標題裡沒有==的故事了??
      • 那hashCode呢??
      • 那把騎士聖劍呢??
    • 然後崩巴也準備開啟營救公主的冒險

本文地址:https://www.cnblogs.com/oberon-zjt0806/p/12367370.html

本文遵循CC BY-NC-SA 4.0協議,轉載請註明出處。
特別說明:

本文的基本語境是Java,如果需要C#版本請看這裡

還有一個故事……(平行世界篇)

這是一個關於另外一個平行世界——Java中的相似的故事……

文藝復興.jpg……

還有一個美麗的夢幻家園:java.util

在Java中,巧了,也有泛型的資料容器。不過,Java的容器和C#的組織方式有些不同,C#是單獨開了一個System.Collections及子名稱空間專門用於給容器類使用,而Java則是把容器連同其他的工具類一起丟到了java.util這一個大包中。
不過,容器的這部分內容似乎在Java裡叫做JCF(Java Collections Framework)

而且,Java似乎不存在非泛型版本的容器,儘管據說SE 5之前的容器普遍存在型別安全性問題(當然已經是過去了……),此外,Java還提供了對應於一些容器的功能介面(而且是泛型介面),方便自定義容器型別,例如,List<E>是列表容器的介面而不是泛型容器,其對應的泛型容器是ArrayList<E>

Pigeon p = new Pigeon("咕咕咕"); // class Pigeon extends Bird
Cuckoo c = new Cuckoo("子規");   // class Cuckoo extends Bird

List<Bird> birds = new List<Bird>() { { add(p); add(c); } };           // 錯誤,List是容器介面,不能直接例項化
ArrayList<Bird> flock = new ArrayList<Bird>() { { add(p); add(c); } }; // 正確,這是一個泛型為Bird的ArrayList容器
List<Bird> avians = new ArrayList<Bird>() { { add(p); add(c); } };      // 正確,ArrayList<E>實現了List<E>,可視為List<E>的多型

匿名內部類(AIC)

這個神奇的初始化寫法在Java術語裡叫做匿名內部類(AIC,Anonymous Inner Class),在Java中AIC是被廣泛使用而且屢試不爽的,主要是用於簡化Java程式碼。AIC的出現使得從一個抽象的介面或抽象類(無法例項化,不提供實現)快速重構一個簡單具體類(可以例項化,具有實現)變得非常容易而無需另開檔案去寫類,而不會造成太大的效能影響(因為AIC是隨用隨丟的)。
不過AIC有個不算副作用的副作用,因為AIC往往需要實現(甚至可能是大量改寫)介面或抽象類的方法,因此可能會在巢狀層數特別多的上下文中使得原本就比較混亂的局面更加混亂(特別是採用了不當的縮排策略的時候,因為AIC的寫法本身在大多數情形下就包含了相當多的巢狀),導致程式碼可讀性嚴重下降,看起來不是很直觀,有礙觀瞻。
此外,如果某個AIC頻繁地出現,那麼AIC就不那麼適用了,這種情況下建議把當前的AIC改成一個具名的類。

並且還有一個善戰的達拉崩巴:HashSet

更加巧合的是,在java.util裡也有一個HashSet<E>,功能也是作為一個雜湊集使用,也就是說它也滿足如下兩點:

  1. 元素是唯一的
  2. 元素是無序的

What a COINCI-DANCE~~

而且,也是要分兩種情況,值型別下,只要兩個值相等,那麼第二個元素就不會被新增:

int i = 5;
int j = 5;

HashSet<int> integers = new HashSet<int>();
integers.add(i); // i被新增到integers中
integers.add(j); // 沒有變化,integers中已經有5了

而對於引用型別來說,和C#類似,也採用引用一致性判斷:

// 為了簡單這裡不封裝了,直接上欄位
class Student {
    public int id; 
    public String name;
    public Student(int id, String name) {
        this.id = id;
        this.name = name;
    }
}

public class Program {
    public static void main(String[] args) {
        Student s1 = new Student(1, "Tom");
        Student s2 = new Student(2, "Jerry");        
        Student s3 = s1;
        Student s4 = new Student(1,"Tom");
        HashSet<Student> students = new HashSet<Student>();
        students.add(s1); // s1被加入students中
        students.add(s2); // s2被加入students中
        students.add(s3); // 沒有變化,s1已存在
        students.add(s4); // s4被加入到students中,儘管s4和s1長得一樣,但引用不一致
    }
}

我甚至是差不多拿上篇文章中的程式碼,幾乎沒怎麼改23333

但是,和上次一樣的問題,儘管s4s1引用不一致,但實際場合下,我們傾向於把它們當作同一個人,那麼怎麼辦呢??

還有另外一個故事(不是虛假傳說)

不是虛假傳說-序言

嗯,這個不是虛假的故事,這就是正經的解決方案,放心大膽的讀吧!!

還有一對塗滿毒藥的奪命雙匕:equals和hashCode

當然,Java裡所有物件都繼承自java.lang.ObjectObject,而Java物件也有兩種相等判別方式:==Object.equals

而且,這倆判別方式一模一樣,值型別下只要值相等就可以,而對於引用型別,==判別的是引用一致性。

但是為什麼這次標題裡沒有==的故事了??

一直就沒有,那是你的錯覺,上一篇的==還是虛假的故事呢,而且原因也很簡單:

Java裡運算子不允許過載。

而且Object裡沒有之前的ReferenceEquals,所以==就是引用一致性的兜底判別,沒法過載的話那就免談了,不過equals是父類方法,當然是可以過載的。

那hashCode呢??

和隔壁的System.Object.GetHashCode()類似地,這邊也有一個java.lang.Object.hashCode(),作用也是類似的,返回一個用作雜湊值的數。

而且更加巧合的是,這裡的Object.equals()hashCode()也沒什麼關係,單獨改寫其中一個函式對另外一個函式也都沒什麼影響。

最最巧合的是,和隔壁一樣,Java也建議equalshashCode要改都改。
不過之前是因為非泛型容器(比如Hashtable),而這次是真真正正的為了泛型容器。

HashSet<E>正是使用equalshashCode作為雙重判據,HashSet<E>認為equals返回true,且兩者hashCode相等的時候,就認為是相同的元素而不被

那把騎士聖劍呢??

非常遺憾,這裡沒有那種東西,java.util並沒有提供類似於IEqualityComparer<T>的東西,而HashSet<E>也不提供getComparator()這種方法……

java.util只提供這個東西——interface Comparator<T>,其作用和C#中的IComparer<T>差不多,因為Java不讓過載運算子,因此Comparator<T>提供了compare方法進行的大小比較,而且只是用於比較排序而已。

然後崩巴也準備開啟營救公主的冒險

最後把程式改寫成這個樣子:

import java.util.HashSet;

class Student {
    public int id;
    public String name;
    public Student(int id,String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public boolean equals(Object obj) {
        // TODO Auto-generated method stub
        return id == ((Student)obj).id && name.equals(((Student)obj).name);
    }
    
    @Override
    public int hashCode() {
        return id;
    }
}

public class HSetTest {
    public static void main(String[] args) {
        Student s1 = new Student(1,"Tom");
        Student s2 = s1;
        Student s3 = new Student(1,"Tom");
        @SuppressWarnings("serial")
        HashSet<Student> students = new HashSet<Student>() {
            {
                add(s1); // s1被新增到students中
                add(s2); // 沒有變化,s1已存在
                add(s3); // 沒有變化,s3被認為和s1邏輯上相等
            }
        };
        
        for(Student s : students) {
            System.out.println(String.format("%d.%s",s.id,s.name));
        }
    }
}

輸出結果:

1.Tom