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:

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.