Java中如何正確重寫equals方法
重寫equals方法的正確打開方式
正文開始@Assassin
1. 什么是equals方法?
我們首先得知道,Object類是 Java中所有類的父類(超類/基類),也就是說,在Java中,所有的類都是默認(rèn)繼承自Object類的,換言之,Object類中所實(shí)現(xiàn)的方法我們都可以直接拿來用。而equals方法便是Object類所實(shí)現(xiàn)的眾多方法之一。
以下截圖自Java11 API

Object類的所有方法:

1.1 equals方法:
equals:是Object類中的方法,只能判斷引用類型,等下可以帶大伙看看jdk源碼- 默認(rèn)判斷的是地址是否相等(因?yàn)橐妙愋妥兞康讓颖举|(zhì)就是來存儲(chǔ)對(duì)象地址的,
有C/C++知識(shí)的小伙伴應(yīng)該很了解),在子類中往往會(huì)重寫該方法,用于判斷對(duì)象的內(nèi)容是否相等。比如等下會(huì)簡(jiǎn)單了解到的Integer和String(在IDEA里看源碼實(shí)現(xiàn))
2. 為什么要重寫equals方法?
我們有Object類實(shí)現(xiàn)的equals方法能用不就行了?為啥還得重寫equals方法呢?這就要看看Object類的equals方法實(shí)現(xiàn)機(jī)制了。

我們可以清楚地看到,Object類的equals方法底層是用 == 來實(shí)現(xiàn)的,也就是說它的用法跟我們平常用來比較基本數(shù)據(jù)類型的 == 用法一致。我們首先來看一下 == 的語法:
- == 只能用來比較基本數(shù)據(jù)類型是否相等,也就是單純的值比較;
- == 在比較浮點(diǎn)數(shù)的時(shí)候也可能存在失效的情況,這是因?yàn)楦↑c(diǎn)數(shù)的存儲(chǔ)機(jī)制跟整型家族不一樣,浮點(diǎn)數(shù)本身就不能表示一個(gè)精確的值(具體原因可自行查看IEEE 754規(guī)則,這里不再展開)
所以我們?cè)趩渭兊倪M(jìn)行基本數(shù)據(jù)類型的值比較時(shí)可以用 == ,而比較引用數(shù)據(jù)類型就不能這么做,前面有提到,引用數(shù)據(jù)類型本質(zhì)上是來引用/存儲(chǔ)對(duì)象的地址的,所有你完全可以把它當(dāng)做C/C++的指針來看待(這里杠一句說Java沒有指針的,個(gè)人覺得只是叫法不同罷了 )
注: 不要把Java引用跟C++引用搞混了,C++引用其實(shí)是指針常量,即int* const,這也是C++的引用只能作為一個(gè)變量的別名的原因。
2.1 舉個(gè)例子吧~
比較兩個(gè)int時(shí)可以直接用 == ,當(dāng)它們相等時(shí)結(jié)果為true,而當(dāng)new了兩個(gè)屬性完全一樣的對(duì)象時(shí),再用 == 來進(jìn)行比較就會(huì)出現(xiàn)錯(cuò)誤,如我們所見,明明我們應(yīng)該想要得到true的,結(jié)果卻是false
源碼:

運(yùn)行結(jié)果:

到這里,我們應(yīng)該大致清楚為啥要在比較對(duì)象時(shí)重寫equals方法了,因?yàn)?code>Object類提供給我們的不好使~~
3. 分析equals源碼:
在進(jìn)行重寫之前,我們依舊來看看Java API中的定義:
public boolean equals(Object obj)
作用:指示某個(gè)其他對(duì)象是否“等于”此對(duì)象。
equals方法在非null對(duì)象引用上實(shí)現(xiàn)等價(jià)關(guān)系:
- 自反性 :對(duì)于任何非空的參考值
x,x.equals(x)應(yīng)該返回true。 - 對(duì)稱性 :對(duì)于任何非空引用值
x和y,x.equals(y)應(yīng)該返回true當(dāng)且僅當(dāng)y.equals(x)回報(bào)true。 - 傳遞性 :對(duì)于任何非空引用值
x,y和z,如果x.equals(y)回報(bào)true個(gè)y.equals(z)回報(bào)true,然后x.equals(z)應(yīng)該返回true。 - 一致性 :對(duì)于任何非空引用值
x和y,多次調(diào)用x.equals(y)始終返回true或始終返回false,前提是未修改對(duì)象上的equals比較中使用的信息。 - 對(duì)于任何非空的參考值
x,x.equals(null)應(yīng)該返回false。
類Object的equals方法實(shí)現(xiàn)了對(duì)象上最具區(qū)別的可能等價(jià)關(guān)系; 也就是說,對(duì)于任何非空引用值x和y ,當(dāng)且僅當(dāng)x和y引用同一對(duì)象( x == y具有值true )時(shí),此方法返回true 。
注意:通常需要在重寫此方法時(shí)覆蓋hashCode方法,以便維護(hù)hashCode方法的常規(guī)協(xié)定,該方法聲明相等對(duì)象必須具有相等的哈希代碼。

接下來看看String類中重寫的equals方法和Integer類中重寫的equals方法:

//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;
}
簡(jiǎn)單解讀一下就是當(dāng)對(duì)比的是同一個(gè)對(duì)象時(shí),直接返回true,提高效率。當(dāng)傳進(jìn)來的對(duì)象是當(dāng)前類的實(shí)例時(shí),進(jìn)入進(jìn)一步的判斷,一個(gè)for循環(huán)依次遍歷字符串每一個(gè)字符,只要有一個(gè)字符不同就返回false。
繼續(xù)來看看Integer類的equals源代碼:

//Integer類的equals源代碼:
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
Integer類的equals源碼簡(jiǎn)單許多,只要傳入的對(duì)象是當(dāng)前類的實(shí)例,就進(jìn)行進(jìn)一步的判斷:當(dāng)它們的值相等時(shí),就返回true,不相等就返回false。
這里還是來實(shí)際演示一下⑧,就以Integer類為例:

很明顯,我們知道Integer類重寫了equals方法且是引用類型。當(dāng)直接用 == 來比較引用類型變量時(shí),結(jié)果是false,而用equals判斷結(jié)果為true。這便很好地說明了重寫equals方法的必要性。String類大伙自己驗(yàn)證一哈⑧。
4. 正確重寫equals方法:
(先說結(jié)論,getClass()比instanceof更安全)
到這里,我們基本把equals方法的各種源碼都分析了一遍,接下來就是我們自己要來實(shí)現(xiàn)equals方法了。
這里提供兩個(gè)比較常見的equals重寫方法:
- 用
instanceof實(shí)現(xiàn)重寫equals方法 - 用
getClass實(shí)現(xiàn)重寫equals方法
假設(shè)有此場(chǎng)景:
在已經(jīng)創(chuàng)建好的長(zhǎng)方形類中重寫Object類中的equals方法為當(dāng)長(zhǎng)方形的長(zhǎng)和寬相等時(shí),返回TRUE,同時(shí)重寫hashCode方法,重寫toString方法為顯示長(zhǎng)方形的長(zhǎng)寬信息。并測(cè)試類。
package com.test10_04;
import java.util.Objects;
class Rectangle {
private double length;
private double wide;
public Rectangle() {
//空實(shí)現(xiàn)
}
public Rectangle(double length, double wide) {
setLength(length);
setWide(wide);
}
public double getLength() {
return length;
}
public void setLength(double length) {
assert length > 0.0 : "您的輸入有誤,長(zhǎng)方形的長(zhǎng)不能小于0";
this.length = length;
}
public double getWide() {
return wide;
}
public void setWide(double wide) {
assert wide > 0.0 : "您的輸入有誤,長(zhǎng)方形的寬不能小于0";
this.wide = wide;
}
public double area() {
return this.length * this.wide;
}
public double circumference() {
return 2 * (this.wide + this.length);
}
public boolean equals(Object obj) {
if (this == obj) { //判斷一下如果是同一個(gè)對(duì)象直接返回true,提高效率
return true;
}
if (obj == null || obj.getClass() != this.getClass()) { //如果傳進(jìn)來的對(duì)象為null或者二者為不同類,直接返回false
return false;
}
//也可以以下方法:
// if (obj == null || !(obj instanceof Rectangle)) { //如果傳進(jìn)來的對(duì)象為null或者二者為不同類,直接返回false
// return false;
// }
Rectangle rectangle = (Rectangle) obj; //向下轉(zhuǎn)型
//比較長(zhǎng)寬是否相等,注意:浮點(diǎn)數(shù)的比較不能簡(jiǎn)單地用==,會(huì)有精度的誤差,用Math.abs或者Double.compare
return Double.compare(rectangle.length, length) == 0 && Double.compare(rectangle.wide, wide) == 0;
}
public int hashCode() { //重寫equals的同時(shí)也要重寫hashCode,因?yàn)橥粚?duì)象的hashCode永遠(yuǎn)相等
return Objects.hash(length, wide); //調(diào)用Objects類,這是Object類的子類
}
public String toString() {
return "Rectangle{" + "length=" + length + ", wide=" + wide + '}';
}
}
public class TestDemo {
public static void main(String[] args) {
Rectangle rectangle1 = new Rectangle(3.0, 2.0);
Rectangle rectangle2 = new Rectangle(3.0, 2.0);
System.out.println(rectangle1.equals(rectangle2));
System.out.println("rectangle1哈希碼:" + rectangle1.hashCode() +
"\nrectangle2哈希碼:" + rectangle2.hashCode());
System.out.println("toString打印信息:" + rectangle1.toString());
}
}
具體實(shí)現(xiàn)思路在代碼中講的很清楚了,我們這里重點(diǎn)分析一下getClass和instanceof兩種實(shí)現(xiàn)方法的優(yōu)缺點(diǎn):

將代碼邏輯簡(jiǎn)化一下:
我們就重點(diǎn)看這段簡(jiǎn)單的代碼
//getClass()版本
public class Student {
private String name;
public void setName(String name) {
this.name = name;
}
@Override
public boolean equals(Object object){
if (object == this)
return true;
// 使用getClass()判斷對(duì)象是否屬于該類
if (object == null || object.getClass() != getClass())
return false;
Student student = (Student)object;
return name != null && name.equals(student.name);
}
//instanceof版本
public class Student {
private String name;
public void setName(String name) {
this.name = name;
}
@Override
public boolean equals(Object object){
if (object == this)
return true;
// 通過instanceof來判斷對(duì)象是否屬于類
if (object == null || !(object instanceof Student))
return false;
Student student = (Student)object;
return name!=null && name.equals(student.name);
}
}
事實(shí)上兩種方案都是有效的,區(qū)別就是getClass()限制了對(duì)象只能是同一個(gè)類,而instanceof卻允許對(duì)象是同一個(gè)類或其子類,這樣equals方法就變成了父類與子類也可進(jìn)行equals操作了,這時(shí)候如果子類重定義了equals方法,那么就可能變成父類對(duì)象equlas子類對(duì)象為true,但是子類對(duì)象equlas父類對(duì)象就為false了,如下所示:
class GoodStudent extends Student {
@Override
public boolean equals(Object object) {
return false;
}
public static void main(String[] args) {
GoodStudent son = new GoodStudent();
Student father = new Student();
son.setName("test");
father.setName("test");
// 當(dāng)使用instance of時(shí)
System.out.println(son.equals(father)); // 這里為false
System.out.println(father.equals(son)); // 這里為true
// 當(dāng)使用getClass()時(shí)
System.out.println(son.equals(father)); // 這里為false
System.out.println(father.equals(son)); // 這里為false
}
}
注意看這里用的是getClass()

返回值兩個(gè)都是false,符合我們的預(yù)期,(連類都不一樣那肯定得為false?。?/p>

而換成instanceof試試看咯:

運(yùn)行結(jié)果:一個(gè)為true一個(gè)為false,很明顯出現(xiàn)問題了。

這里的原因如下:
instanceof的語法是這樣的:
當(dāng)一個(gè)對(duì)象為一個(gè)類的實(shí)例時(shí),結(jié)果才為true。但它還有一個(gè)特點(diǎn)就是,如果當(dāng)這個(gè)對(duì)象時(shí)其子類的實(shí)例時(shí),結(jié)果也會(huì)為true。這便導(dǎo)致了上述的bug。也就是說當(dāng)比較的兩個(gè)對(duì)象,他們的類是父子關(guān)系時(shí),instanceof可能會(huì)出現(xiàn)問題。**需要深究的小伙伴可以自己去了解一哈,所以在這里建議在實(shí)現(xiàn)重寫equals方法時(shí),盡量使用getClass來實(shí)現(xiàn)。
在重寫equals方法的同時(shí)需要重寫hashCode方法,具體原因可能后續(xù)會(huì)講到~~
到此這篇關(guān)于Java中如何正確重寫equals方法的文章就介紹到這了,更多相關(guān)Java 重寫 equals方法內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot+Redis隊(duì)列實(shí)現(xiàn)Java版秒殺的示例代碼
本文主要介紹了SpringBoot+Redis隊(duì)列實(shí)現(xiàn)Java版秒殺的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-06-06
springboot jpaRepository為何一定要對(duì)Entity序列化
這篇文章主要介紹了springboot jpaRepository為何一定要對(duì)Entity序列化,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12
使用java?實(shí)現(xiàn)mqtt兩種常用方式
在開發(fā)MQTT時(shí)有兩種方式一種是使用Paho Java 原生庫來完成,一種是使用spring boot 來完成,這篇文章主要介紹了使用java?實(shí)現(xiàn)mqtt兩種方式,需要的朋友可以參考下2022-11-11
一篇文章帶你復(fù)習(xí)java知識(shí)點(diǎn)
以下簡(jiǎn)單介紹了下我對(duì)于這些java基本知識(shí)點(diǎn)和技術(shù)點(diǎn)的一些看法和心得,這些內(nèi)容都源自于我這些年來使用java的一些總結(jié),希望能夠給你帶來幫助2021-06-06
MyBatis-Plus實(shí)現(xiàn)條件查詢的三種格式例舉詳解
本文主要介紹了MyBatis-Plus三中條件查詢格式的示例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-08-08
Java如何替換RequestBody和RequestParam參數(shù)的屬性
近期由于接手的老項(xiàng)目中存在所有接口中新增一個(gè)加密串來給接口做一個(gè)加密效果,所以就研究了一下Http請(qǐng)求鏈路,發(fā)現(xiàn)可以通過?javax.servlet.Filter去實(shí)現(xiàn),這篇文章主要介紹了Java替換RequestBody和RequestParam參數(shù)的屬性,需要的朋友可以參考下2023-10-10

