平時很難遇到需要覆蓋equals的情況。
什么時候不需要覆蓋equals?
如果要問什么時候需要覆蓋equals?答案正好和之前的問題相反。即,類需要一個自己特有的邏輯相等概念,而且超類提供的equals不滿足自己的行為。(PS:對于枚舉而言,邏輯相等和對象相等都是一回事。)
既然只好覆蓋equals,我們就需要遵守一些規(guī)定:
其實這些規(guī)定隨便拿出一個都是很好理解的。難點(diǎn)在于,當(dāng)我遵守一個規(guī)定時有可能違反另一個規(guī)定。
自反性就不用說了,很難想想會有人違反這一點(diǎn)。
關(guān)于對稱性,下面提供一個反面例子:
class CaseInsensitiveString { PRivate final String s; public CaseInsensitiveString(String s) { if (s == null) this.s = StringUtils.EMPTY; else this.s = s; } @Override public boolean equals(Object obj) { if (obj instanceof CaseInsensitiveString) return s.equalsIgnoreCase(((CaseInsensitiveString) obj).s); if (obj instanceof String) return s.equalsIgnoreCase((String) obj); return false; }}這個例子顯然違反對稱性,即x.equals(y)為true 但 y.equals(x)為false。不僅是在顯示調(diào)用時,如果將這種類型作為泛型放到集合之類的地方,會發(fā)生難以預(yù)料的行為。
而對于上面這個例子,在equals方法中我就不牽扯其他類型,去掉String實例的判斷就可以了。
關(guān)于傳遞性,即,當(dāng)x.equals(y)為true 且 y.equals(z)為true 則 x.equals(z)為true。這個規(guī)定在對類進(jìn)行擴(kuò)展時尤其明顯。
比如,我用x,y描述某個Point:
class Point { private final int x; private final int y; public Point(int x, int y) { super(); this.x = x; this.y = y; } @Override public boolean equals(Object obj) { if (!(obj instanceof Point)) return false; Point p = (Point) obj; return p.x == x && p.y == y; }}現(xiàn)在我想給Point加點(diǎn)顏色:
class ColorPoint extends Point { private final Color color; public ColorPoint(int x, int y, Color color) { super(x, y); this.color = color; } @Override public boolean equals(Object obj) { if (!(obj instanceof ColorPoint)) return false; return super.equals(obj) && ((ColorPoint) obj).color == color; }}似乎很自然的提供了ColorPoint的equals方法,但他連對稱性的沒能滿足。于是我們加以修改,令其滿足對稱性:
@Overridepublic boolean equals(Object obj) { if (!(obj instanceof Point)) return false; if (!(obj instanceof ColorPoint)) return obj.equals(this); return super.equals(obj) && ((ColorPoint) obj).color == color;}好了,接下來我們就該考慮傳遞性了。比如我們現(xiàn)在有三個實例,1個Point和2個ColorPoint....然后很顯然,不滿足<當(dāng)x.equals(y)為true 且 y.equals(z)為true 則 x.equals(z)為true>。 事實上,我們無法在擴(kuò)展可實例化類的同時,既增加新的值組件,又保留equals約定。
于是我索性不用instanceof,改用getClass()。這個確實可以解決問題,但很難令人接受。如果我有一個子類沒有覆蓋equals,此時equals的結(jié)果永遠(yuǎn)是false。
既然如此,我就放棄繼承,改用復(fù)合(composition)。以上面的ColorPoint作為例子,將Point變成ColorPoint的field,而不是去擴(kuò)展。 代碼如下:
public class ColorPoint { private final Point point; private final Color color; public ColorPoint(int x, int y, Color color) { if (color == null) throw new NullPointerException(); point = new Point(x, y); this.color = 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 point.hashCode() * 33 + color.hashCode(); }}關(guān)于一致性,即如果兩者相等則始終相等,除非有一方被修改。這一點(diǎn)與其說equals方法,到不如思考寫一個類的時候,這個類應(yīng)該設(shè)計成可變還是不可變。如果是不可變的,則需要保證一致性。
考慮到這些規(guī)定,以下是重寫equals時的一些建議:
任何覆蓋了equals方法的類都需要覆蓋hashCode方法。忽視這一條將導(dǎo)致類無法與基于散列的數(shù)據(jù)結(jié)構(gòu)一起正常工作,比如和HashMap、HashSet和Hashtable。
下面是hashCode相關(guān)規(guī)范:
在程序執(zhí)行期間,只要對象的equals方法的比較操作所用到的信息沒有被修改,那么對這個對象調(diào)用多少次hashCode,起結(jié)果必須始終如一地返回同一個證書。如果是同一個程序執(zhí)行多次,每次調(diào)用的結(jié)果可以不一致。
如果兩個對象根據(jù)equals方法比較是相等的,那么兩個對象的hashCode結(jié)果必須相同。
如果兩個對象根據(jù)equals方法比較是不相等的,那么這兩個對象的hashCode不一定返回不同的結(jié)果。但是,如果不同的對象返回不同的hashCode,則能提高散列表的性能。
下面的代碼是一個反面例子:
import java.util.HashMap;import java.util.Map;public final class PhoneNumber { private final short areaCode; private final short prefix; private final short lineNumber; public PhoneNumber(int areaCode, int prefix, int lineNumber) { rangeCheck(areaCode, 999, "area code"); rangeCheck(prefix, 999, "prefix"); rangeCheck(lineNumber, 9999, "line number"); this.areaCode = (short) areaCode; this.prefix = (short) prefix; this.lineNumber = (short) lineNumber; } private static void rangeCheck(int arg, int max, String name) { if (arg < 0 || arg > max) throw new IllegalArgumentException(name + ": " + arg); } @Override public boolean equals(Object o) { if (o == this) return true; if (!(o instanceof PhoneNumber)) return false; PhoneNumber pn = (PhoneNumber) o; return pn.lineNumber == lineNumber && pn.prefix == prefix && pn.areaCode == areaCode; } // Broken - no hashCode method! // A decent hashCode method - Page 48 // @Override public int hashCode() { // int result = 17; // result = 31 * result + areaCode; // result = 31 * result + prefix; // result = 31 * result + lineNumber; // return result; // } public static void main(String[] args) { Map<PhoneNumber, String> m = new HashMap<PhoneNumber, String>(); m.put(new PhoneNumber(707, 867, 5309), "Jenny"); System.out.println(m.get(new PhoneNumber(707, 867, 5309))); }}通過equals方法比較,兩個實例在邏輯上是相等的。但由于沒有覆蓋hashCode方法,兩個實例返回的hashCode是不同的。在散列表中,如果散列碼不匹配,就不必檢查兩個實例是否相等。如果隨便提供這樣的一個hashCode方法:
public int hashCode(){ return 42;}這樣會讓散列表失去優(yōu)勢,退化為鏈表。
最好的hashCode應(yīng)該是<不同的對象產(chǎn)生不同的散列碼>。即,散列函數(shù)把集合中不同的實例均勻地分布到所有可能的散列值上。
下面是一種簡單的思路(也就是上面例子中注釋的部分):
針對每一個關(guān)鍵的field(假設(shè)變量名為f)計算int類型的散列碼,不同類型有不同的計算方式。
注意,這里僅限關(guān)鍵field。對于那些用其他field值計算出來的field,我們可以將其排除在外。
如果一個類是不可變的,而且計算散列值的開銷比較大,我們可以試著將散列值緩存?;蛘呶覀円部梢栽囋囇舆t初始化,在hashCode第一次被調(diào)用時進(jìn)行初始化:
private volatile int hashCode; // (See Item 71)@Override public int hashCode() { int result = hashCode; if (result == 0) { result = 17; result = 31 * result + areaCode; result = 31 * result + prefix; result = 31 * result + lineNumber; hashCode = result; } return result;}另外,Josh Bloch在最后加了一段話:
Many classes in the Java platform libraries, such as String, Integer, and Date, include in their specifications the exact value returned by their hashCode method as a function of the instance value. This is generally not a good idea, as it severely limits your ability to improve the hash function in future releases.
<可以把它們的hashCode方法返回的確切值規(guī)定為該實例的一個函數(shù)。> 看了翻譯后一頭霧水...
后來在爆棧中看到這么一個回復(fù),記下來作為參考:
The API docs specify that String.hashCode() is computed by a specific formula. Client code is free to independently compute the hash code using that exact formula and assume it will be the same as that returned by String.hashCode(). This might seem perverse for pure Java code, but does make some sense with JNI. There are probably other cases where it would make sense to take advantage of the extra knowledge that the API specifies.
新聞熱點(diǎn)
疑難解答