本文參考
本篇文章參考自《Effective Java》第三版第十條"Obey the general contract when overriding equals"
the conditions when each instance of the class is equal only to itself
- Each instance of the class is inherently unique —— 類的每一個實例本就彼此不同,例如Thread類,每一個線程僅和自身相等
- There is no need for the class to provide a "logical equality" test —— 該類不具備"邏輯相等"的特點,例如兩個字符串的相等需要比較二者每一個字符是否相等,因為它具備"邏輯相等"的特點(這些類可以被稱為"值類"),而某些類的設計,如Pattern類,則沒有提供equals()方法的重載來判斷兩個正則表達式是否相等,即不需要考慮"邏輯相等的情況"
- A superclass has already overridden equals, and the superclass behavior is appropriate for this class —— 父類的equals()方法同樣適用於子類,所以子類不需要再進行重載,例如絕大多數的Set實現類都繼承了AbstractSet的equals()方法,下面是AbstractSet的equals()方法源碼,顯然已適用於絕大多數的Set實現
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof Set))
return false;
Collection<?> c = (Collection<?>) o;
if (c.size() != size())
return false;
try {
return containsAll(c);
} catch (ClassCastException unused) {
return false;
} catch (NullPointerException unused) {
return false;
}
}
- The class is private or package-private, and you are certain that its equals method will never be invoked —— 這個類是私有的或是包級私有的,可以確保它的equals()方法不會被調用,若被調用,可以將equals()方法設計為返回異常
value classes
前文已經講到了"值類"具備"邏輯相等"的特點,但是注意並不是所有的"值類"都需要重載equals()方法,例如在"用靜態工廠方法代替構造器"中我們提到了Boolean類的靜態構造方法valueOf(booean b),它返回兩個固定的類實例new Boolean(true)和new Boolean(false),若能夠確保每個值至多只存在一個實例,則不需要重載equals()方法
枚舉類型是其中一個特例
One kind of value class that does not require the equals method to be overridden is a class that uses instance control to ensure that at most one object exists with each value
general contract
- Reflexive: For any non-null reference value x, x.equals(x) must return true
- Symmetric: For any non-null reference values x and y, x.equals(y) must return true if and only if y.equals(x) returns true
- Transitive: For any non-null reference values x, y, z, if x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) must return true
- Consistent: For any non-null reference values x and y, multiple invocations of x.equals(y) must consistently return true or consistently return false, provided no information used in equals comparisons is modified
- For any non-null reference value x, x.equals(null) must return false
上述分別是自反性、對稱性、傳遞性、一致性和x.equals(null) == false
當我們重載equals()方法時,應該問自己三個問題:它是否是對稱的、傳遞的、一致的
There is no way to extend an instantiable class and add a value component while preserving the equals contract
我們無法在擴展(繼承)可實例化的類的同時,既增加新的值組件,同時又保留equals約定,這在Java的Timestamp類中也有體現,Timestamp類繼承了java.util.Date類,下面是源碼注釋
The Timestamp.equals(Object) method never returns true when passed an object that isn't an instance of java.sql.Timestamp, because the nanos component of a date is unknown. As a result, the Timestamp.equals(Object) method is not symmetric with respect to the java.util.Date.equals(Object) method. Also, the hashCode method uses the underlying java.util.Date implementation and therefore does not include nanos in its computation.
原文還指出了一種錯誤的寫法例子,首先是父類Point的聲明
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override public boolean equals(Object o) {
if (!(o instanceof Point))
return false;
Point p = (Point)o;
return p.x == x && p.y == y;
}
@Override public int hashCode() {
return 31 * x + y;
}
}
其次是子類ColorPoint類的聲明
public class ColorPoint extends Point {
private final Color color;
public ColorPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
// Broken - violates symmetry
@Override public boolean equals(Object o) {
if (!(o instanceof ColorPoint))
return false;
return super.equals(o) && ((ColorPoint) o).color == color;
}
}
當ColorPoint類的實例調用equals()方法和Point類的實例進行比較時,總是返回false,顯然違背了對稱性
但是我們可以通過"單向關聯",將原本的"父類"作為成員變量加入到原本的"子類"中,通過非繼承的方式,來遵守equals()方法的約定
// Adds a value component without violating the equals contract
public class ColorPoint {
private final Point point;
private final Color color;
public ColorPoint(int x, int y, Color color) {
point = new Point(x, y);
this.color = Objects.requireNonNull(color);
}
/**
* Returns the point-view of this color point.
*/
public Point asPoint() {
return point;
}
@Override public boolean equals(Object o) {
if (!(o instanceof ColorPoint))
return false;
ColorPoint cp = (ColorPoint) o;
return cp.point.equals(point) && cp.color.equals(color);
}
@Override public int hashCode() {
return 31 * point.hashCode() + color.hashCode();
}
}
do not write an equals method that depends on unreliable resources
倘若equals()方法依賴於不可靠的資源,可能會違背"一致性"約定,因為在不同的場合下調用equals()方法時,可能會有不同的結果
例如Java中的URL類,在源碼注釋中我們可以看到equals方法被標注為不符合"一致性"約定
The defined behavior for equals is known to be inconsistent with virtual hosting in HTTP
因為URL類實例的比較涉及IP地址是否相等,而IP地址可能會在不同網絡環境下發生變化,所以是"不可靠"資源,無法滿足"一致性"約定
here's a recipe for a high-quality equals method
- Use the == operator to check if the argument is a reference to this object
- Use the instanceof operator to check if the argument has the correct type
- Cast the argument to the correct type
- For each "significant" field in the class, check if that field of the argument matches the corresponding field of this object
- Always override hashCode when you override equals(item 11)
上述提到的AbstractSet類的equals()方法就是一個很好的例子
public boolean equals(Object o) {
// Use the == operator to check if the argument is a reference to this object
if (o == this)
return true;
// Use the instanceof operator to check if the argument has the correct type
if (!(o instanceof Set))
return false;
// Cast the argument to the correct type
Collection<?> c = (Collection<?>) o;
// check if that field of the argument matches the corresponding field of this object
if (c.size() != size())
return false;
try {
return containsAll(c);
} catch (ClassCastException unused) {
return false;
} catch (NullPointerException unused) {
return false;
}
}
特別的,對於浮點型數值的比較,Java提供了Float.compare()和Double.compare()方法來考慮Float.NaN、-0.0f和0.0f這樣特殊的情況,而它們重載的equals()方法沒有對這種情況進行考慮,下面是equals()方法的源碼注釋
If f1 and f2 both represent Float.NaN, then the equals method returns true, even though Float.NaN==Float.NaN has the value false.
If f1 represents +0.0f while f2 represents -0.0f, or vice versa, the equal test has the value false, even though 0.0f==-0.0f has the value true.
因此,在比較兩個值類實例的大小前,還要注意它的特殊取值
Idea自動生成equals()方法
public class TestAutoEquals {
private String username;
private int age;
private boolean male;
private String password;
}
idea提供了不同的"自動化"實現方式
- idea default
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TestAutoEquals that = (TestAutoEquals) o;
if (age != that.age) return false;
if (male != that.male) return false;
if (!username.equals(that.username)) return false;
return password.equals(that.password);
}
用getClass代替instanceof運算符來判斷是否是同一個類的實例,盡管這樣做能夠解決上述"There is no way to extend an instantiable class and add a value component while preserving the equals contract"的問題,但有時候不能夠采用這種替換方案,例如繼承自同一個接口的不同實現類之間的比較或是父類和子類之間的比較
- Apache commons-lang / commons-lang3
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TestAutoEquals that = (TestAutoEquals) o;
return new EqualsBuilder()
.append(age, that.age)
.append(male, that.male)
.append(username, that.username)
.append(password, that.password)
.isEquals();
}
由特殊的Builder模式實現對每個成員變量的相等判斷
- Google guava / Java 7+
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TestAutoEquals that = (TestAutoEquals) o;
return age == that.age &&
male == that.male &&
Objects.equal(username, that.username) &&
Objects.equal(password, that.password);
}
此處的Objects可以來自java.util包,也可以來自com.google.common.base包
另外可以由Google開發的AutoValue框架自動生成equals()方法,並且可以結合builder構建者模式,將在下一篇文章中講解