"一致性相等"的陷阱
關(guān)于Object類中的equals()方法與Comparable接口中的compareTo()方法之間有何種關(guān)聯(lián),之前還真沒考慮過。通過java.net看到此文之后,收獲了一點(diǎn)兒新知識,希望大家也能如此。(2012.12.09最后更新)方法equals()與Comparable接口中的compareTo()方法是Java中最基本的兩個方法之一,然而它們的定義卻圍繞著"與相等一致"這一有趣的概念。
equals()方法
Java中的equals()方法既明確,又模糊。Java清楚地定義了如何準(zhǔn)確地檢驗(yàn)一個equals()方法是可用的。一個恰當(dāng)?shù)膃quals()方法必須是自反的,對稱的,可傳遞的,一致的,并能處理null引用。
然而equals()方法又是不清晰的。Javadoc說到,該方法指定了其它對象是"等于"這個對象的。注意,"等于"是放在引號中的。此處的關(guān)鍵就是,它沒有定義如何去判定這種相等性。
- 對象的一致性(==)默認(rèn)是繼承自O(shè)bject類
- 對象的整體可觀測的狀態(tài),例如,若兩個對象是相等的,那么在應(yīng)用的其它部分可以用一個對象去替代另一個對象。
- 對象信息中的某些部分,如ID,使得檢驗(yàn)對象相等性在邏輯上是有意義的。
Comparable接口定義了可比較性的概念。Javadoc指出compareTo()方法"強(qiáng)制設(shè)定了每個實(shí)現(xiàn)了該接口的類的對象的全部順序"。
實(shí)現(xiàn)了Comparable接口的類有一個天然的排序,這可便于存儲,也能在不使用單獨(dú)的Comparator的情況下,用于像TreeSet和TreeMap這樣的集合對象。
該接口的定義明晰,它要求其實(shí)現(xiàn)必須確保對稱性與傳遞性,就像equals()方法那樣。
一致性/非一致性相等
Comparable接口有如下描述:
類C的天然排序意味著要與equals()方法保持一致,只有當(dāng)且僅當(dāng)e1.compareTo(e2) == 0與e1.equals(e2)有相同的布爾值。
基本上,這就要求由compareTo()定義的相等性與equals()方法定義的相等性具有相同的概念(除去有null的情況)。乍一看,該要求很簡單,但實(shí)際上它有其復(fù)雜性,后面將會討論到。
當(dāng)考慮到操作符重載時,這種定義就特別有用。若我們假設(shè)有一種類Java語言,在這種語言中,==并不表示對象的同一性,而是通過方法去進(jìn)行比較,大于/小于操作符也是如此,問題是調(diào)什么樣的方法。在類Java語言中大于/小于天然地就要基于compareTo()方法,而==則要調(diào)用equals()方法。
// our new Java-like language
if (a < b) return "Less"; // translation ignoring nulls: if (a.compareTo(b) < 0)
if (a > b) return "Greater"; // translation ignoring nulls: if (a.compareTo(b) > 0)
if (a == b) return "Equal"; // translation ignoring nulls: if (a.equals(b))
throw new Exception("Impossible assuming no nulls?");
但如果compareTo()方法不是"一致性相等",那么上述代碼將會拋出異常,因?yàn)楫?dāng)a.equals(b)為false時,a.compareTo(b)會返回0。if (a < b) return "Less"; // translation ignoring nulls: if (a.compareTo(b) < 0)
if (a > b) return "Greater"; // translation ignoring nulls: if (a.compareTo(b) > 0)
if (a == b) return "Equal"; // translation ignoring nulls: if (a.equals(b))
throw new Exception("Impossible assuming no nulls?");
在集合,如TreeMap,中還會發(fā)生其它問題:
// Foo class is "inconsistent with equals"
assert foo1.equals(foo2) == false;
assert foo1.compareTo(foo2) == 0;
TreeMap<Foo, String> map =
map.put(foo1, "a");
map.put(foo2, "b");
當(dāng)使用equals()方法時,這兩個對象不相等,但使用compareTo()時,它們卻相等。在這種情況下,該Map的元素個數(shù)將為1,而非0。assert foo1.equals(foo2) == false;
assert foo1.compareTo(foo2) == 0;
TreeMap<Foo, String> map =

map.put(foo1, "a");
map.put(foo2, "b");
由于這些"一致性相等"的問題,Javadoc說道"強(qiáng)烈建議(盡管并不要求)天然排序規(guī)則要與equals()方法保持一致"。
JDK中的許多類為了符合"一致性相等"這一規(guī)范而實(shí)現(xiàn)了Comparable接口。這些類包括Byte,Short,Integer,Long,Character和String。
還有些更有趣的類:
BigDecimal--肯定是"非一致性相等",比如4.00與4.0不一致,但進(jìn)行比較時,認(rèn)為它們是一樣的。
Double/Float--該類顯式地提供了排序規(guī)則,并為正零和負(fù)零,以及NaN都提供了相等性檢查,以確保它的compareTo()方法符合"一致性相等"。
CharSet--該類基于ID或名稱。equals()方法對待字段串是大小寫敏感的,但compareTo()方法卻不這樣。雖然名稱一般會符合某種標(biāo)準(zhǔn),但這是一種值得懷疑的"一致性"。
*Buffer(nio)--該簇類的比較基于緩沖存放的內(nèi)容,在我的測試中equals()和compareTo()是"一致的"。
Rdn(ldap)--該類的比較基于狀態(tài)的標(biāo)準(zhǔn)化格式,因此也是"一致性相等"。
ObjectStreamField(序列化)--該類的比較基于名稱,但會首先對基本數(shù)據(jù)類型進(jìn)行排序。因?yàn)闆]有覆蓋equals()方法,所以是"非一致性相等"。
...
注意:對于大多數(shù)的例子,我都不得不查看其源代碼或編寫測試程序以確定該類是不是符合"一致性相等"。這兒有一個不錯地清理Javadoc和檢驗(yàn)UUID equals()方法的Adopt-a-JDK任務(wù)。
JSR-310
一直看到許多關(guān)于BigDecimal的問題,已有計(jì)劃將帖子顯示這將造成多么大的爭議。
基本上,為某些類定義equals()和compareTo()看起來很容易。LocalDate表示某單一日歷系統(tǒng)中的某個日期,所以它有一個顯而易見的排序算法和相等規(guī)則。LocalTime則表示某個時刻,所以它也有一個明顯的排序算法和相等規(guī)則。Instant表示時間線上的某個時刻,那么它的排序與相等也是顯見的。
但在其它的情況下,這就不是那么顯而易見了。考慮這樣一個類OffsetDateTime:
dt1 = OffsetDateTime.parse("2012-11-05T06:00+01:00");
dt2 = OffsetDateTime.parse("2012-11-05T07:00+02:00");
這樣的兩個日期-時刻對象代表時間線上一個相同的時刻點(diǎn),但它們有不同的本地時,而且相對的UTC/格林威治時間的偏移量也不相同。dt2 = OffsetDateTime.parse("2012-11-05T07:00+02:00");
那么就有一個問題要留給讀者們...你更傾向于如下哪種觀點(diǎn)...
1. dt1不等于dt2,compareTo()分別比較本地時與偏移量,使用"一致性相等"(使用獨(dú)立的Comparator基于時刻對其進(jìn)行排序)。
2. dt1不等于dt2,compareTo()基于時間線的上時刻點(diǎn),使用"非一致性相等"。
3. dt1等于dt2,compareTo()基于時間線的上時刻點(diǎn),使用"一致性相等"。
4. dt1等于dt2,且不實(shí)現(xiàn)Comparable接口。
5. dt1不等于dt2,且不實(shí)現(xiàn)Comparable接口。
我個人更傾向于讓dt1.equals(dt2)返回true這種方案,但我仍持開放態(tài)度。
順便地,也可以將這個問題提給BigDecimal,如果你能修改這個類,使其符合"一致性相等",你會修改它的equals()方法,還是compareTo()方法?