前言
本篇部落客要梳理一下Java中對象比較的需要注意的地方。
==和equals()方法
在前面對
String
介紹時,談到過使用
==
和
equals()
去比較對象是否相等的問題。使用
==
比較的是兩個對象在記憶體中的位址是否一緻,也就是比較兩個對象是否為同一個對象。使用
equals()
方法可以依據對象的值來判定是否相等。
equals()方法是根類Object的預設方法,檢視Object中equals()的預設實作:
public boolean equals(Object obj) {
return (this == obj);
}
可看出沒有重寫過的
equals()
方法和
==
是一樣的,都是比較兩個對象引用指向的記憶體位址是否一樣判斷兩個對象是否相等。
在介紹String時,我們發現并沒有重寫過equals()方法,但是可以使用equals()正确判斷兩個字元串對象是否相等。檢視String源碼可以發現是String本身重寫了equals()方法。
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
Java中很多類都自身重寫了equals()方法,但是要使我們自定義的對象能正确比較,我們就需要重寫equals()方法。
public class Student{
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override //此關鍵字可以幫助我們檢查是否重寫合乎要求
public boolean equals(Object obj) {
if (this == obj) //檢測this與obj是否指向同一對象。這條語句是一個優化,避免直接去比較同一對象的各個域
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass()) // 比較this和obj是否屬于同一個類 若是兩個對象都不是同一個類的 則不相等
return false;
Student other = (Student) obj; //将obj轉換成相應的Student類型
//對所有需要比較的域進行比較 基本類型使用== 對象域使用equal 數組類型的域,可以使用靜态的Arrays.equals方法檢測相應的數組元素是否相等
if (age != other.age)
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
public static void main(String[] args) {
Student stu1 = new Student("sakura",20);
Student stu2 = new Student("sakura",20);
System.out.println(stu1.equals(stu2)); //output: true
}
}
以上重寫的equals()方法是考慮最為全面的,推薦使用這種方式。當然人性化的IDE會幫助我們寫代碼,如eclipse,就有快捷鍵幫助我們自動生成此格式的equals()方法。
hashCode()方法和equals()方法
在上圖中,
hashCode()
equals()
是配套自動生成的。為什麼要附加生成hashCode()呢?
hashCode()是根類Object中的預設方法,檢視JDK文檔:
hashCode()方法與equals()方法沒有任何關系,hashCode()的存在是為了服務于建立在散清單基礎上的類,如Java容器的HashMap, HashSet等。hashCode()方法擷取對象的哈希碼(散列碼)。
哈希碼是一個int型的整數,用于确定對象在哈希表(散清單)中的索引位置。
hashCode()方法會根據不同的對象生成不同的哈希值,預設情況下為了確定這個哈希值的唯一性,是通過将該對象的内部位址轉換成一個整數來實作。
下面我們看一個例子:
public static void main(String[] args) {
Student stu1 = new Student("sakura",20);
Student stu2 = new Student("sakura",20);
HashSet<Student> stuSet = new HashSet<>();
stuSet.add(stu1);
stuSet.add(stu2);
System.out.println(stu1.equals(stu2));
System.out.println(stu1);
System.out.println(stu2);
System.out.println(stuSet);
}
/*
output:
true
prcatice.Student@7852e922
prcatice.Student@4e25154f
[prcatice.Student@7852e922, prcatice.Student@4e25154f]
*/
HashSet不會存儲相同的對象。按理來說,stu1和stu2是相等的,不應該被重複放進stuSet裡面。但是結果顯示,出現了重複的對象。因為stu1和stu2的hashCode()傳回值不同,是以它們将會被存儲在stuSet中的不同的位置。
對象存儲在HashSet中時,先會根據對象的哈希值來檢視是否哈希表中相應的索引位置是否有對象,若是沒有則直接将對象插入;若是該位置有對象,則使用equals判斷該位置上的對象與待插入的對象是否為相同對象,兩個對象相等則用新值重新整理舊值,不相等就将待插入對象挂在已存在對象的後面(單連結清單挂載)。
是以,要使stu1和stu2不能都被插入stuSet中,則要在Student中重寫hashCode()方法。
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + age;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
在hashCode()中加入31這個奇素數來計算哈希值目的是為了減少哈希沖突(在同一位置插入多個對象)。詳細理由可以參考此篇博文:為什麼在定義hashcode時要使用31這個數呢?
然後我們在運作一次程式的輸出如下:
/*
true
prcatice.Student@c9c6a694
prcatice.Student@c9c6a694
[prcatice.Student@c9c6a694]
*/
Comparator接口和Comparable接口
我們使用equals()方法可以實作比較我們自定義類的對象是否相等,但是卻無法得到對象誰大誰小的關系。Java中提供了兩種方式來使得對象可以比較大小,實作
Comparator
接口或者
Comparable
接口。
Comparable接口
以
able
結尾的接口都表示擁有某種能力。若是某個自定義類實作了comparable接口,則表示
該類的執行個體對象擁有可以比較的能力實作comparable接口需要覆寫其中的compareTo()方法。
int compareTo(T o)
- 傳回負數:目前對象小于指定比較的對象;
- 傳回0,兩個對象相等;
- 傳回正數,目前對象大于指定比較的對象。
public class Student implements Comparable<Student>{
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
//重寫comparaTo方法 以age作為标準比較大小
@Override
public int compareTo(Student o) {
return return (this.age<o.age ? -1 : (this.age == o.age ? 0 : 1));;//本類接收本類對象,對象可以直接通路屬性(取消了封裝的形式)
}
@Override
public String toString() {
return "name:" +name + " age:"+age;
}
public static void main(String[] args) {
Student stu1 = new Student("sakura",20);
Student stu2 = new Student("sakura",21);
Student stu3 = new Student("sakura",19);
//TreeSet會對插入的對象進行自動排序,是以要求知道對象之間的大小
TreeSet<Student> stuSet = new TreeSet<>();
stuSet.add(stu1);
stuSet.add(stu2);
stuSet.add(stu3);
//使用foreach(), lambda表達式輸出stuSet中的值 forEach()方法從JDK1.8才開始有
stuSet.forEach(stu->System.out.println(stu));
}
}
/*
output:
name:sakura age:19
name:sakura age:20
name:sakura age:21
*/
實作了comparaTo()方法使用age為标準升序排序。也可以以name為标準排序,或者其他自定義的比較依據。
但是當Student已經實作了以age為依據從小到大排序後,我們又想以name為依據排序,在這個簡單的程式中可以直接将
return this.age-o.age
變為
return this.name.compareTo(o.name)
(name為String對象)。
但是這樣修改類結構會顯得十分麻煩,萬一在以後的程式中遇到的是别人封裝好的類不能直接改類結構又該怎麼辦。
有沒有其他友善的比較方法,實作對象的大小比較。辦法是有的,那就是實作Comparator接口。
Comparator接口
實作Comparator接口需要重寫其中的compare()方法。
int compare(T o1,T o2)
根據第一個參數小于、等于或大于第二個參數分别傳回負整數、零或正整數,通常使用-1, 0, +1表示。
需要注意,Comparator接口中也有一個equals方法,但是這是判斷該比較器與其他Comparator比較器是否相等。
public class Student {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "name:"+name + " age:"+age;
}
public static void main(String[] args) {
Student stu1 = new Student("sakuraamy",20);
Student stu2 = new Student("sakurabob",21);
Student stu3 = new Student("sakura",19);
ArrayList<Student> stuList = new ArrayList<>();
stuList.add(stu1);
stuList.add(stu2);
stuList.add(stu3);
//沒有必要去建立一個比較器類 采用内部類的方式實作Comparator接口
Collections.sort(stuList, new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
return (o1.age<o2.age ? -1 : (o1.age == o2.age ? 0 : 1));
//return o1.name.compareTo(o2.name);
}
});
//或者使用lambda表達式
//Collections.sort(stuList, (o1,o2)->o1.age-o2.age);
System.out.println(stuList);
}
}
/*
[name:sakura age:19, name:sakuraamy age:20, name:sakurabob age:21]
*/
由上可見,實作Comparator接口比較對象比實作Comparable接口簡單和靈活。
使用這兩個接口比較對象都需要注意幾點:
- 對稱性:若存在compare(x, y)>0 則 compare(y, x) <0,反之亦然
- 傳遞性:((compare(x, y)>0) && (compare(y, z)>0)) 可以推導出compare(x, z)>0
- 相等替代性:compare(x, y)0可以推導出compare(x, z)compare(y, z)
補充
在Comparable中沒有使用簡潔明了的this.age-o.age作為傳回值,是因為這是一個常見的程式設計錯誤。它隻能在this.age和o.age都是無符号的整數時才能正确工作。而Java隻支援有符号數,是以這種方式就存在潛在危險。this.age是很大的正整數而o.age是很大的負整數,二者相減就會溢出進而産生負值,導緻錯誤結果。
小結
簡單總結一下本篇關于Java中對象比較的内容:
- 要比較自定義類的對象是否相等需要重寫equals()方法;
- 當對象要存儲在建立在哈希表基礎上的容器中時,還需要重寫hashCode()方法用于判定對象在集合中的存儲位置;
- 以某種依據比較對象的大小,可以實作Comparable接口或者Comparator接口,前者需要在類中實作表示該類擁有可以比較的能力,後者是在類外實作一個比較器,可以使用多種規則對對象進行比較,更靈活。
參考:
[1] Eckel B. Java程式設計思想(第四版)[M]. 北京: 機械工業出版社, 2007
[2] 掘金. JAVA 對象比較中的坑[EB/OL]. /2018-12-01. https://juejin.im/entry/586c6a6061ff4b006407e2b9.
每天進步一點點,不要停止前進的腳步~