昨天說了注意事項,今天就來談談,覆寫equals
要遵守的Reflexive、Symmetric、Transitive、Consistent和Non-nullity這五個規則,以及可以依照哪些步驟,一步一步完成覆寫equals
。
物件使用equals
確認跟自己相不相同時,必須回傳true
,也就是x.equals(x) = true
。想像一下,如果x.equals(x)
是false,用list的contains
確認物件有沒有在list之中的時候,會很弔詭的回傳false。
若x.equals(y) = true
,則y.equals(x) = true
,確認兩個物件相不相同時,必須要對稱,互相確認的結果都要一致。如果兩個物件的equals邏輯不一致,有可能會造成x.equals(y)
和y.equals(x)
的結果不同。
以下面的範例來說,如果Person
和Employee
這兩個物件的identityNumber
(身分證字號)一致的話,表示這兩個物件是相同的,用equals
判斷回傳的結果應該要是true
,但是Person
並沒有在覆寫equals
的時候做這個處理,所以person.equals(employee)
的結果是false
,跟employee.equals(person)
的結果不一樣。
import java.util.Objects;
class Person {
private String name;
private int age;
private String identityNumber;
public Person(String name, int age, String identityNumber) {
this.name = name;
this.age = age;
this.identityNumber = identityNumber;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (obj instanceof Person) {
Person person = (Person) obj;
return Objects.equals(identityNumber, person.identityNumber);
}
return false;
}
public String getIdentityNumber() {
return identityNumber;
}
}
public class Employee {
private int employeeId;
private String employeeName;
private int age;
private String identityNumber;
public Employee(int employeeId, String employeeName, int age, String identityNumber) {
this.employeeId = employeeId;
this.employeeName = employeeName;
this.age = age;
this.identityNumber = identityNumber;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (obj instanceof Person) {
Person person = (Person) obj;
return Objects.equals(identityNumber, person.getIdentityNumber());
} else if (obj instanceof Employee) {
Employee employee = (Employee) obj;
return Objects.equals(employeeId, employee.employeeId);
}
return false;
}
public static void main(String[] args) {
Person person = new Person("John", 25, "R123456");
Employee employee = new Employee(123, "John", 25, "R123456");
System.out.println(person.equals(employee)); // false
System.out.println(employee.equals(person)); // true
}
}
如果對稱性有問題,在使用list的contains
時,一樣會有物件明明有在list裡面,卻找不到該物件的問題。
若有x, y, z三個物件,x.equals(y) = true
且y.equals(z) = true
,x.equals(z) = true
也要成立。但有的時候,邏輯上真的無法符合這個特性,也許就要改成別的寫法,舉例來說,有Point
和ColorPoint
兩個類別,ColorPoint
繼承Point
,但是多了顏色的屬性,在覆寫equals
的時候,如果ColorPoint
的位置跟Point
一樣,就認定他們是相等的,但如果是兩個ColorPoint
的物件,必須顏色和位置都一樣才會相等,這樣就無法滿足Transitive的特性,因為x.equals(y) = true
且y.equals(z) = true
但x.equals(z) = false
。
import java.util.Objects;
class Point {
private int x;
private int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Point)) return false;
Point point = (Point) obj;
return x == point.x && y == point.y;
}
}
class ColorPoint extends Point {
private String color;
public ColorPoint(int x, int y, String color) {
super(x, y);
this.color = color;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj instanceof ColorPoint) return super.equals(obj) && color.equals(((ColorPoint) obj).color);
if (!(obj instanceof Point)) return false;
super.equals(obj);
return super.equals(obj);
}
public static void main(String[] args) {
Point point = new Point(1, 2);
ColorPoint colorPoint1 = new ColorPoint(1, 2, "red");
ColorPoint colorPoint2 = new ColorPoint(1, 2, "blue");
// 比較傳遞性
System.out.println(colorPoint1.equals(point)); // true
System.out.println(point.equals(colorPoint2)); // true
System.out.println(colorPoint1.equals(colorPoint2)); //false
}
}
這個例子在邏輯上合理,在覆寫equals
的規則不合理,比較適合的方式就是調整ColorPoint
和Point
的關係,讓他們從繼承變成composition,並且唯有point
和color
都一致的情形下,兩個物件才會相等。
import java.util.Objects;
class Point {
private int x;
private int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
class Color {
private String code;
public Color(String code) {
this.code = code;
}
}
class ColorPoint {
private Point point;
private Color color;
public ColorPoint(int x, int y, Color color) {
point = new Point(x, y);
this.color = color;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof ColorPoint)) return false;
ColorPoint colorPoint = (ColorPoint) obj;
return point.equals(colorPoint.point) && color.equals(colorPoint.color);
}
public static void main(String[] args) {
Color color1 = new Color("red");
ColorPoint colorPoint1 = new ColorPoint(1, 2, color1);
Color color2 = new Color("blue");
ColorPoint colorPoint2 = new ColorPoint(1, 2, color2);
Point point = new Point(1, 2);
System.out.println(colorPoint1.equals(colorPoint2)); //false
System.out.println(colorPoint2.equals(colorPoint1)); //false
System.out.println(colorPoint1.equals(point)); // false
}
}
不管觸發幾次x.equals(y)
的結果必須一致。這個規則對immutable的物件沒有什麼問題,但如果是mutable的物件,由於物件內的值有可能隨時間和不同使用方式而有不同,所以有可能不是每一次觸發equals()
的結果都會一樣,舉例來說,如果java.net.URL
的equals()
確認相同的依據是IP address,就有可能發生一開始是相同後面卻變成不同的問題,除非是固定IP,不然IP有可能會重新分配,這樣就不符合Consistent這個規則。如果真的需要對mutable的物件覆寫equals
,判斷的依據就不能是值會一直變動的欄位。
任何物件用equals
檢查跟null
有沒有相等時,必須是false
。這個可以用檢查物件型別的方式去確認,null
不等於任何一個型別,所以會回傳false
。
@Override
public boolean equals(Object obj) {
if (!(obj instanceof ColorPoint)) return false;
ColorPoint colorPoint = (ColorPoint) obj;
return point.equals(colorPoint.point) && color.equals(colorPoint.color);
}
綜合以上的規則和幾個範例,覆寫equals
的時候可以參考下面的步驟:
==
檢查傳進來的物件是否等同於目前這個物件,如果是的話,直接回傳true
。@Override
public boolean equals(Object obj) {
if (this == obj) return true;
}
instanceof
檢查傳進來的物件的型別,是否等同於指定的型別,如果不是的話,直接回傳false
。@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Point)) return false;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Point)) return false;
Point point = (Point) obj;
}
true
。檢查的方式,對於primitive型別的欄位(ex: int
),除了float
和double
之外,可以直接使用==
,float
和double
可以使用Float.compare
和Double.compare
,因為float
和double
有NaN和-0.0f的問題,所以交由套件處理會比較好。array的話,在Java 1.5開始,可以使用Arrays.equals
判斷。@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Point)) return false;
Point point = (Point) obj;
return x == point.x && y == point.y;
}
equals
有符合Symmetric、Transitive和Consistent。