compareTo方法并沒有在Object中聲明。相反,它是Comparable接口中唯一的一個(gè)方法。compareTo方法不但允許進(jìn)行簡單的同等性比較,而且允許執(zhí)行順序比較,除此之外,它與Object的equals方法具有相似的特性,還是個(gè)涉及到泛型的方法。類實(shí)現(xiàn)了Comparable接口,就表明它的實(shí)例具有內(nèi)在的排序關(guān)系(natural ordering)。給實(shí)現(xiàn)Comparable接口的對(duì)象數(shù)組進(jìn)行排序,只需要下面這一行代碼:
Arrays.sort(a);
對(duì)于存儲(chǔ)在集合中的Comparable對(duì)象進(jìn)行搜索、計(jì)算極限值以及自動(dòng)維護(hù)也是同樣的簡單。例如下面的程序依賴于String實(shí)現(xiàn)了Comparable接口,它去掉了命令行參數(shù)列表中的重復(fù)參數(shù),并按字母表順序打印出來了:
public class WordList {
public static void main(String[] args) {
Set s = new TreeSet();
Collection.addAll(s, args);
System.out.println(s);
}
}
一旦類實(shí)現(xiàn)了Comparable接口,它就可以跟許多泛型算法以及依賴于該接口的集合實(shí)現(xiàn)進(jìn)行協(xié)作。你付出了很小的努力就可以獲得非常強(qiáng)大的功能。事實(shí)上,Java平臺(tái)類庫中的所有值類都實(shí)現(xiàn)了Comparable接口。如果我們需要寫一個(gè)類時(shí),當(dāng)這個(gè)類有非常明顯的內(nèi)在排序關(guān)系,我們就應(yīng)該優(yōu)先考慮實(shí)現(xiàn)這個(gè)接口:
public interface Comparable {
int compareTo(T t);
}
Comparable接口的規(guī)范
compareTo方法的通用約定和equals方法的很相似,將一個(gè)對(duì)象與指定對(duì)象進(jìn)行比較。當(dāng)該對(duì)象小于、等于或者大于指定對(duì)象的時(shí)候,分別返回一個(gè)負(fù)整數(shù)、零或者正整數(shù)。如果由于指定對(duì)象的類型而無法和該對(duì)象進(jìn)行比較,則拋出ClassCastException異常。
在下面的說明中,符號(hào)sgn(表達(dá)式)表示數(shù)學(xué)中的signum函數(shù),它根據(jù)表達(dá)式的值為負(fù)值、零和正值,分別返回-1、0、1。
確保所有的屬性都滿足sgn(x.compareTo(y)) == -sgn(y.compareTo(x))。當(dāng)y.compareTo(x)拋出異常時(shí),x.compareTo(y)也要拋出異常。這條規(guī)則和equls規(guī)范里面的對(duì)稱性類似。
必須確保比較關(guān)系是可傳遞的:(x.compareTo(y) > 0 && y.compareTo(z) > 0)同時(shí)x.compareTo(z) > 0也成立。對(duì)應(yīng)著equals使用規(guī)范里面的傳遞性。
必須確保x.compareTo(y) == 0同時(shí)所有的z都滿足sgn(x.compareTo(z)) == sgn(y.compareTo(z))。
強(qiáng)烈建議(x.compareTo(y) == 0) == (x.equals(y)),但是并不是絕對(duì)必要的。如果一個(gè)類實(shí)現(xiàn)了Comparable接口,并且違反了這個(gè)條件,我們應(yīng)該明確予以說明。推薦使用這樣的說法:“注意,該類具有內(nèi)在的排序功能,但是與equals不一致”。
在類的內(nèi)部,任何合理的順序關(guān)系都可以滿足compareTo的約定。在跨越不同類的時(shí)候,compareTo可以不做比較:如果兩個(gè)被比較的對(duì)象引用不同的對(duì)象,compareTo可以拋出ClassCastException異常。通常,這正是compareTo在這種情況下應(yīng)該做的事情,如果類設(shè)置了確定的參數(shù),這也正式它要做的事情。雖然以上約定沒有把跨類之間的比較排除在外,但是從Java1.6發(fā)行版本開始,Java平臺(tái)類庫中就沒有支持跨類比較的這種特性了。
就好像違反了hashCode約定的類會(huì)破壞其他依賴于散列算法的做法的情況一樣,違反了compareTo約定的類也會(huì)破壞其他依賴于比較關(guān)系的類。依賴于比較關(guān)系的類包括有序集合類TreeSet和TreeMap、以及工具類Collections和Arrays,它們內(nèi)部包含有搜索和算法排序。
上面的三個(gè)條款的一個(gè)直接結(jié)果就是,有compareTo方法施加的同等性測(cè)試,也一定遵守相同于equals約定所施加的限制條件:自反性、對(duì)稱性和傳遞性。因此,下面的告誡也同樣的適用:無法在用新的值組件擴(kuò)展可實(shí)例化的類時(shí),同時(shí)保持compareTo約定,除非愿意放棄面對(duì)對(duì)象的抽象優(yōu)勢(shì)。如果你想為一個(gè)實(shí)現(xiàn)了Comparable接口的類增加值組件,不擴(kuò)展這個(gè)類;要便攜一個(gè)不相關(guān)的類,其中包含第一個(gè)類的一個(gè)實(shí)例。然后提供一個(gè)“視圖(view)”方法返回這個(gè)實(shí)例。這樣既可以讓你自由地在第二個(gè)類上實(shí)現(xiàn)compareTo方法,同時(shí)也允許它的客戶端在必要的時(shí)候,把第二個(gè)類的實(shí)例看作第一個(gè)類的實(shí)例。運(yùn)用組合優(yōu)于繼承。
compareTo約定的最后一段是一個(gè)強(qiáng)烈的建議,而不是真正的規(guī)則,只是說明了compareTo方法施加的同等性測(cè)試,在通常情況下就應(yīng)該返回與equals方法同樣的結(jié)果。如果遵守了這一條規(guī)定,那么由compareTo方法所施加的關(guān)系順序就會(huì)被認(rèn)為”于equals一致”。如果違反了這條規(guī)則,則會(huì)相反。如果一個(gè)類的compareTo方法施加了一個(gè)與equals方法不一致的順序關(guān)系,它仍然能夠工作,但是如果有一個(gè)有序集合包含了該類的元素,這個(gè)集合就可能無法遵守相應(yīng)集合接口(Collection、Set或Map)的通用約定。這是因?yàn)?,?duì)于這些接口的通用約定是按照equals方法來定義的,但是有序集合時(shí)使用由compareTo方法而不是equals方法所施加的同等性測(cè)試。
例如,考慮BigDecimal類,它的compareTo方法和equals方法不一致。如果你創(chuàng)建了一個(gè)HashSet實(shí)例,并且添加了new BigDecimal(“1.0”)和new BigDecimal(“1.00”),這個(gè)集合就將包含兩個(gè)元素,因?yàn)樾略龅郊现械膬蓚€(gè)BigDecimal實(shí)例,通過equals方法來比較的時(shí)候是不相等的。然而如果你使用TreeSet而不是HashSet來執(zhí)行同樣的過程,集合中將只包含一個(gè)元素,因?yàn)檫@兩個(gè)BigDecimal實(shí)例在通過compareTo方法進(jìn)行比較的時(shí)候是相等的。
編寫compareTo方法與編寫equals方法非常相似,但也存在幾處重大的差別。因?yàn)镃ompareable接口時(shí)參數(shù)化的,而且comparable方法是靜態(tài)的類型,因此不必進(jìn)行類型檢查,也不需要對(duì)它的參數(shù)進(jìn)行類型的轉(zhuǎn)換。如果參數(shù)的類型不合適,這個(gè)調(diào)用甚至無法編譯。如果參數(shù)為null,這個(gè)調(diào)用。如果參數(shù)為null,這個(gè)調(diào)用應(yīng)該拋出NullPointerException異常,并且一旦該方法試圖訪問它的成員變量時(shí)就應(yīng)該拋出。
CompareTo方法中的域的比較是順序的比較,而不是同等性的比較。比較對(duì)象引用域可以是通過遞歸地調(diào)用compareTo方法來實(shí)現(xiàn)。如果一個(gè)域并沒有實(shí)現(xiàn)Compareable接口,或者你需要使用一個(gè)非標(biāo)準(zhǔn)的排序關(guān)系,就可以使用一個(gè)顯示的Comparator來代替?;蛘呤蔷帉懽约旱腃omparator,或者是使用已有的Comparator,例如(第八條沒有實(shí)現(xiàn)這個(gè)方法,此例比較方法是jdk中的方法)針對(duì)下面的這個(gè)類,已經(jīng)有一個(gè)compareTo方法:
public final class CaseInsensitiveString implements Comparable {
public int comparaTo(CaseInsensitiveString cis) {
return String.CASE_INSENSITIVE_ORDER.compare(s, cis.s);
}
... //Remainder omitted
}
比較整數(shù)型基本類型的域,可以使用關(guān)系操作符<和>。例如,浮點(diǎn)域用Double.compare或者Float.compare,而不同關(guān)系操作符,當(dāng)應(yīng)用到浮點(diǎn)值得時(shí)候,它們沒有遵守compareTo的通用約定。對(duì)于數(shù)組域,則要把這些知道原則應(yīng)用到每個(gè)元素上。
如果一個(gè)類有多個(gè)關(guān)鍵域,那么比較這些關(guān)鍵域的順序非常關(guān)鍵。必須從最關(guān)鍵的域開始,逐步進(jìn)行到所有的重要域。如果某個(gè)域的比較產(chǎn)生了非零的結(jié)果(0代表著相等),則整個(gè)比較操作結(jié)束,并返回該結(jié)果。如果最關(guān)鍵的域是相等的,則再比較下一個(gè)關(guān)鍵域,以此類推,如果所有域都是相等的,那么才返回0。例如下面的例子:
public final class PhoneNumber implements Comparable {
private final short areaCode;
private final short prefix;
private final short lineNumber;
public PhoneNumber(int areaCode, int prefix,
int lineNumber) {
this.areaCode = (short) areaCode;
this.prefix = (short) prefix;
this.lineNumber = (short) lineNumber;
}
@Override
public int compareTo(PhoneNumber pn) {
if (areaCode < pn.areaCode)
return -1;
if(areaCode > pn.areaCode)
return 1;
if (prefix < pn.prefix)
return -1;
if (prefix > pn.prefix)
return 1;
if (lineNumber < pn.lineNumber)
return -1;
if (lineNumber > pn.lineNumber)
return 1;
return 0;
}
}
雖然這個(gè)方法可行,但它還可以進(jìn)行改進(jìn)。先來看看compareTo方法的約定,并沒有指定返回值的大小(magnitude),而只是指定了返回值的符號(hào)??梢岳眠@一點(diǎn)來簡化代碼,或許還可以提高它的運(yùn)行速度:
public int compareTo(PhoneNumber pn) {
int areaCodeDiff = areaCode - pn.areaCode;
if (areaCodeDiff != 0)
return areaCodeDiff;
int prefixDiff = prefix - pn.prefix;
if (0 != prefixDiff)
return prefixDiff;
return lineNumber - pn.lineNumber;
}
使用這種方法的時(shí)候需要注意,有符號(hào)的32位整數(shù)還不足以大到能夠表達(dá)任意兩個(gè)32位整數(shù)的差值,如果i是一個(gè)很大的正整數(shù),j是一個(gè)很小的負(fù)整數(shù),i-j有可能會(huì)溢出,并且返回一個(gè)負(fù)值= 。