JE_11: Zvažte implementování Comparable
Na rozdíl od jiných metod, popisovaných v této kapitole, není metoda compareTo deklarovaná ve třídě Object. Je to jediná metoda rozhraní java.lang.Comparable. Svým charakterem se podobá metodě equals třídy Object, jenom kromě prostých porovnání rovnosti umož�uje také porovnávání pořadí. Implementováním Comparable třída indikuje, že její instance mají přirozené řazení. Seřazení pole objektů implementujících Comparable je velmi jednoduché:
Array.sort(a);
Podobně sandné je hledání, výpočet extrémních hodnot a správa automaticky řazených kolekcí objektů Comaprable. Například následující program, který spoléhá na to, že String impementuje Comaprable, tiskne abecedně seřazený seznm argumentů příkazového řádku, přičemž vynechává duplikáty:
public class WordList { public static void main(String[] args) { Set s = new TreeSet(); s.addAll(Arrays.asList(args)); System.out.println(s); } }
Implementováním Comparable umož�uejte své třídě spolupracovat se všemi z mnoha obecných impementací algoritmů a kolekcí, které se na toto rozhraní spoléhají. Získáte velké možnosti jen s malou námahou. Prakticky všechny hodnotové tříd v knihovnách platformy Java implementují Comparable. Píšete-li nějakou hodnotovou třídu se zřejmým přirozeným řazením, jako je například abecední pořadí, číselné pořadí nebo chronologické pořadí, měli byste se vážně zamyslet nad implementováním tohoto rozhraní. Tato rada vám vysvětlí, jak postupovat.
Obecný kontrakt metody comapreTo se svým charakerem podobá kontraktu metody equals. Zde jej máme zkopírovaný ze specifikace Comaprable:
-
Porovnává tento objekt se specifikovaným objektem a určuje pořadí. Vrací záporné celé číslo, nulu nebo kladné elé čísl, je-li tento objekt menší než, rovný nebo větší než zadaný objekt.Vyvolává ClassCastException, pokud typ zadaného objektu neumož�uje jeho porovnání s tímto objektem.
V následujícím popisu představuje zápis sgn(výraz) matematickou funkci signum, která podle definice vrací -1, 0 nebo 1 podle toho, zda je hodnota výrazu záporná, nulová nebo kladná.
Implementátor musí zajistit, že výraz sgn(x.compareTo(y)) == =sgn(y.compareTo(x)) pro všechny x a y (to také znamená, že x.compareTo(y) musí vyvolat výjimku tehdy a právě tehdy, když výjimku vyvolá y.compareTo(x)).
- Implementátor musí tké zajistit, že je tento vztah tranzitivní: (x.compareTo(y) > 0 && y.compareTo(z) > 0) implikuje x.comapreTo(z) > 0.
- Implementátor musí také zajistit, aby výraz x.comapreTo(y) == 0 implikoval, že sgn(x.comapreTo(z)) == sgn(y.comapreTo(z)) pro všechna z.
- Doporučuje se, třebaže to není striktně nutné, aby platilo (x.compareTo(y) == 0) == (x.equals(y)). Obecně by měla jakákoli třída impementující rozhraní Comparable a narušující uvedenou podmínku tuto skutečnost jasně deklarovat. Doporučený tvar zní: "Poznámka: Tato třída má přirozené pořadí, které není konzistentní s rovnostmi.".
Nenechte se zastrašit matematickou podstatou tohoto kontraktu. Podobně jako kontrakt equals (rada 7) není ani kontrakt compareTo tak složitý, jak vypadá. Ve třídě naplní kontrakt compaerTo jakýkoli rozumný vztah pořadí. V rámci více tříd nemusí compareTo narozdíl od equals fungovat: je možné vyvolat výjimku ClassCastException, pokud se porovnávané odkazy na objekty odkazují na objekty různých tříd. Právě to by v takovém případě měla metoda compareTo obvykle udělat. Třebaže kontrakt samotný nezakazuje porovnávání mezi třídami, lespo� ve verzi 1.4 knihoven platformy Java neexistují žádné třídy, které by takové chování podporovaly.
Podobně jako může třída porušující kontrakt hashCode narušit další třídy závislé na hešování, může také třída porušující kontrakt compareTo narušit další třídy závisející na porovnávání. Mezi třídy závisející na porovnání patří řazné kolekce, TreeSet a TreeMap, a pomocné třídy Collections a Arrays, které obsahují algoritmy hledání a řazení.
Projděme si podmínky kontraktu comapreTo. první podmínka říká, že když změníte pořadí porovnávání dvou objektů, pak dojde k očekávanému: je-li první obejkt menší než druhý, pak musí být druhý objekt větší než první; je-li první objekt rovný druhému, pak musí být také druhý objekt roven prvnímu; je-li první objekt větší než druhý, pak musí být druhý objekt menší než ten první. Druhý předpoklad říká, že je-li první objekt větší než druhý a druhý je větší než třetí, pak musí být také první objekt větší než ten třetí. Poslední předpoklad říká, že všechny objekty, které se vyhodnotí jako rovné, musí poskytnout stejný výsledek i při porovnání s jakýmkoli jiným z těchto objektů.
Jedním z důsledků uvedených tří předpokladů je to, že test orvnosti používaný metodou compareTo musí vyhovovat stejným omezením, která klade kontrakt eqals: refelxivita, symetričnost, tranzitivita a nenullovost. Proto platí i stejné omezení: prostě neexistuje žádný způsob, jak rozšířit třídu, z níž lze vytvářet instance, o nějaký nový aspekt a přitom zachovat kontrakt comapreTo (rada 7. Platí i stejné řešení. Chcete-li přidat nějaký významný aspekt uričté třídě implementující Comparable, pak ji nerozšiřujte; napište nesouvisející třídu obsahující atribut dané první třídy. Pak posyktněte metodu "pohledu", která vrací tento atribut. Ve druhé třídě můžete následně implementovat libovolnou metodu compareTo a přitom můžete umožnit svému klientovi podívat se v případě potřeby na instanci této druhé třídy jako na instanci třídy první.
Poslední odstavec kontraktu compaerTo, který je spíše výrazným doporučením než skutečnou podmínkou, praví, že test rovnosti používaný metodou compareTo by měl obecně vracet stejné výsledky, jako metoda equals. Pokud tento předpoklad naplníte, pak se o pořadí vytvářeném takovou metodou compareTo říká, že je konzistentní rovnostmi. Je-li podmínka porušena, pak je řazení nekonzistentní rovnostmi. Třída, jejíž metoda compareTo používá pořadí, které není konzistentní rovnostmi, bude sice fungovat, ale řazené kolekce obsahující prvky takové třídy nemusejí napl�ovat obecný kontrakt příslušných rozhraní kolekcí (Collection, Set nebo Map). Je-li tomu tak proto, že obecný kontrakt těchto rozhraní je definován v termínech metody equals, ale řazené kolekce používají test rovnosti zajišťovaný pomocí compareTo namísto equals. Dojde-li k tomu, není to sice katastrofa, ale měli byste si být tohoto problému vědomi.
Zvažme třídu BigDecimal,jejíž metoda compareTo není konzistentní s metodou equals. vytvoříte-li HashSet a přidáte-li nový BigDecimal("1.O") a nový BigDecimal("1.00"), pak bude tato množina obsahovat dva prvky, protože obě instance BigDecimal přidané do množiny si nejsou rovné připorovnávání metodou equals. Pokud však vykonáte stejnou proceduru prostřednictvím TreeSet namísto HashSet, pak bude množina obsahovat jen jeden prvek, protože obě instance BigDecimal jsou si při porovnávání metodou compareTo rovné (podrobnosti najdete v dokumentaci BigDecimal).
Psaní metody compareTo se podobá psaní metody equals, je tu však několik klíčových rozdílů. Před převáděním nemusíte kontrolovat typ argumentu. Nemá-li argument příslušný typ, pak by měla metoda compareTo vyvolat výjimku ClassCastException. Je-li argument null, pak by měla metoda compareTo vyvolat výjimku NullPointerExeption. A právě toto chování získáte, když prostě převedete argument na správný typ a pak se pokusíte přístoupit k jeho členům.
Samotná porovnávání atributů jsou porovnáváními pořadí spíše než porovnáváními rovnosti. Atribut odkazů na obejtky porovnávejt rekurzivním voláním metody compareTo. Pokud nějaký atribut neimplementuje Comparable nebo potřebujete-li použít určité nestandardní pořadí, můžete použít explicitní Comparator. buď si napište vlastní nebo použíjte již existující, jako je tomu v následující metodě compareTo třídy CaseInsensitiveString z rady 7:
public int compareTo(Object o) { CaseInsensitiveString cis = (CaseinsensitiveString)o; return String.CASE_INSENSITIVE_ORDER.compare(s, cis.s); }
Primitivní atributy porovnávejte pomocí relačních operátorů < a > a pole porovnávejte aplikováním těchto zásad na každý z prvků. Má-li třída více významných atributů, pak je zásadní pořadí, v němž je porovnáváte. Musíte začít nejvýznamnějším atributem a pak postupovat hlouběji. Je-li výsledek porovnání jiný než nulový (což představuje rovnost), pak jste hotovi; prostě jen tak vraťte výsledek. Jsou-li si nejvýznamnější atributy rovny, pokračujte porovnáním druhých nejvýznamnějších atributů atd. Jsou-li si rovny všechny atributy, pak jsou si rovny i objekty; vraťte nulu. Tuto techniku demonstruje následující metoda compareTo třídy PhoneNumber z rady 8:
public int compareTo(Object o) { PhoneNumber pn = (PhoneNumber)o; // Porovnání kódů oblasti if (areaCode < pn.areaCode) return -1; if (areaCode > pn.areaCode) return 1; // Kódy oblasti jsou si orvny, porovnáme ústředny if (exchange < pn.exchange) return -1; if (exchange > pn.change) return 1; // Kódy oblastí a ústředny jsou si rovny, porovnáme rozšíření if (extension < pn.extension) return -1; if (extension > pn.extension) return 1; return 0; // Všechny atributy jsou si rovny }
Třebaže tato metoda funguje dobře, lze ji vylepšit. Vzpome�te si, že kontrakt compareTo nespecifikuje velikost návratové hodnoty, ale pouze její znaménko. Toho můžete využít ke zjednodušení kódu a pravděpodobně také zajištění jeho trochu rychlejšího vykonávání:
public int compareTo(object o) { PhoneNumber pn = (PhoneNumber)o; // Porovnávání kódů oblastí int areaCodeDiff = areaCode - pn.areaCode; if (areaCodeDiff != 0) return areaCodeDiff; // Kódy oblastí jsou si rovny, porovnáme ústředny int exchangeDiff = exchange - pn.exchange; if (exchangeDiff != O) return exchangeDiff; // Kódy oblastí a ústředny jsou si rovny, porovnáme rozšíření return extension - pn.extension; }
Uvedený trik v tomto případě funguje, musíte jej však používat velmi opatrně. Nedělejte to, pokud si nejste jisti, že daný atribut nemůže být záporný, nebo obecněji, že rozdíl mezi nejnižšími a nejvyššími možnými hodnotami atributů je menší než nebo roven hodnotě Integer.MAX_VALUE (231 - 1). tento trik nebude fungovat obecně, protože 32-bitové celé číslo se znaménkem není dostatečně velké, aby dokázalo reprezentovat rozdíl mezi dvěma libovolnými 32-bitovými celými čísly se znaménky. Pokud je i velké kladné int a j je velké záporné int, pak dojde při (i - j) k přetečení a vrátí se záporná hodnota. Metoda compareTo pak nebude fungovat. Bude vracet nesmyslné výsledky pro určité argumenty a poruší také první a druhou zásadu kontraktu compareTo. Nejedná se o čistě teoretický problém, protože už zapříčinil selhání v reálných systémech. Taková selhání se obtížně odhalují, jelikož chybná metoda compareTo funguje správně pro velmi mnoho vstupních hodnot.