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


本文地址: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


免責聲明!

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



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