V páté části seriálu o využití integrovaného vývojového prostředí Eclipse pro vývoj aplikací v programovacím jazyku Java si nejprve ukážeme, jak je možné s využitím funkcí IDE automaticky vytvořit metody hashCode() a equals(), které se velmi často musí deklarovat například pro objekty ukládané do kolekcí (seznamů, množin, asociativních polí). Taktéž si ukážeme, jakým způsobem je možné jednoduše chování těchto dvou metod otestovat přímo ve vytvářené aplikaci.
Obrázek 1: Při popisu vlastností integrovaného vývojového prostředí Eclipse budeme i dnes využívat projekt s dvojicí zdrojových kódů, který jsme si vytvořili v předcházející části tohoto seriálu. Tyto zdrojové kódy obsahují deklaraci třídy Person a Main.
Překrytí metody equals()
Při tvorbě reálných aplikací s využitím programovacího jazyka Java poměrně často narazíme na nutnost překrytí metody equals(). Připomeňme si, že tato metoda je deklarována již ve třídě Object, tj. ve třídě stojící na samém vrcholu hierarchie tříd v Javě. Tato metoda by měla vracet pravdivostní hodnotu true v tom případě, že jsou dva objekty považovány za ekvivalentní. Pod slovem „ekvivalentní“ se ovšem skrývá velké množství různých nuancí a záleží jen na programátorovi, který u svých tříd sám může určit, jak se bude zjišťovat ekvivalence dvou instancí těchto tříd. Ve třídě Object je použita velmi jednoduchá varianta metody equals(), která vrací pravdivostní hodnotu true pouze tehdy, pokud leží oba porovnávané objekty (přesněji řečeno datové položky těchto objektů – atributy) na stejném místě na haldě (heapu). Jinými slovy to znamená, že použití metody equals() je v tomto případě prakticky totožné s chováním operátoru ==, ostatně právě s využitím tohoto operátoru je metoda Object.equals() implementována (připomeňme si, že v Javě nelze chování operátoru == předefinovat, na rozdíl od programovacího jazyka C++).
Obrázek 2: Zdrojový kód metody equals() definované ve třídě Object stojící na vrcholu hierarchie všech tříd v Javě. Implementace této metody je skutečně velmi jednoduchá, protože se zde pouze porovnávají reference dvou objektů.
V praxi nám však takto definované chování metody equals() nemusí dostačovat, protože většinou požadujeme, aby se porovnávaly nikoli pouze reference na objekty (což je sice velmi rychlé, ale současně i poměrně striktní), ale stavy dvou objektů, přičemž stav je tvořen hodnotami všech atributů, popř. ve speciálních případech pouze vybraných atributů. Dobrým příkladem může být implementace metody equals() ve třídě java.lang.String, popř. v některé „obalové“ třídě vytvořené nad některým primitivním datovým typem (tyto obalové třídy se jmenují java.lang.Integer, java.lang.Float, java.lang.Double atd.). Z tohoto důvodu se programátoři často uchylují k překrytí metody equals() a nahrazují ji svojí vlastní implementací. Pro správné chování, například pro uložení objektů do kolekcí, je však zapotřebí dodržet relativně velké množství požadavků, především požadavku na reflexivitu (x.equals(x) musí vrátit hodnotu true pro každý existující objekt), tranzitivitu, symetričnost (x.equals(y)==y.equals(x)) a konzistenci (metoda by pro stejný objekt vždy měla vracet stále stejnou hodnotu).
Obrázek 3: Implementace metody equals() ve třídě java.lang.String, v níž se porovnávají jednotlivé znaky za předpokladu, že nejsou porovnávány objekty se stejnou referencí a současně se v obou případech jedná o instance třídy String.
Překrytí metody hashCode()
S trochou nadsázky by se mohlo říci, že ruku v ruce s překrytím metody equals() by měli programátoři současně provést i překrytí metody hashCode(), tj. měli by vytvořit vlastní implementaci této metody takovým způsobem, aby chování metod equals() a hashCode() bylo konzistentní, tj. aby například pro dva objekty, pro něž platí x.equals(y)==true (a současně samozřejmě i symetrický vztah y.equals(x)==true), platilo taktéž x.hashCode()==y.hashCode(). Zatímco metoda equals() pracuje s dvojicí objektů a vrací pravdivostní hodnotu true nebo false, u metody hashCode() se pracuje pouze s objektem, pro nějž je metoda volána (metoda je tedy bezparametrická) a vrací se celočíselná hodnota typu int, tj. 32bitové číslo se znaménkem. Tato hodnota by v ideálním případě měla být pro shodné objekty vždy stejná a pro různé objekty vždy různá. To však v mnoha případech není možné zaručit, už jenom z toho důvodu, že stavový prostor většiny objektů je větší než rozsah int (kupříkladu stačí, aby objekt obsahoval jako svůj atribut řetězec či hodnotu typu double atd.).
Obrázek 4: Zdrojový kód metody hashCode() definované ve třídě Object stojící na vrcholu hierarchie všech tříd v Javě. Vidíme, že se tato metoda vytváří v nativní funkci, jejíž zdrojový text je zobrazen v pravé části obrázku (jedná se o implementaci realizovanou v OpenJDK, další implementace JDK/JRE samozřejmě mohou používat odlišnou implementaci).
Hešovací kód objektu by se měl vypočítat z vhodné kombinace hodnot všech atributů tohoto objektu, popř. ve speciálních případech z kombinace hodnot těch atributů, které jsou porovnávány v těle metody equals(). Asi nejlépe je toto chování možné ilustrovat na způsobu implementace seznamů (lists), tj. na třídách implementujících rozhraní java.util.List. Metoda hashCode() v případě seznamů musí postupně zjistit hešovací kódy všech prvků seznamu a tyto kódy vhodným způsobem zkombinovat takovým způsobem, že se původní (postupně počítaný hešovací kód) vynásobí vhodným prvočíslem (zde je konkrétně použita hodnota 31) a přičte se k němu hešovací kód právě zpracovávaného prvku. Samozřejmě je pravděpodobné, že při výpočtu hešovací hodnoty několikrát dojde k jejímu přetečení přes hodnotu 0x7ffffff (MAX_INT), což však nevadí. Metoda hashCode() s těmito vlastnostmi je v případě OpenJDK implementována ve třídě java.util.AbstractList, která je děděna i třídami java.util.ArrayList a java.util.LinkedList – viz též zdrojový kód třídy java.util.AbstractList, jenž je zobrazený na obrázku číslo 5.
Obrázek 5: Implementace metody hashCode() ve třídě java.util.AbstractList, která je děděna i třídami java.util.ArrayList a java.util.LinkedList.
Podobným způsobem je metoda hashCode() implementována i ve třídě String. I zde se totiž postupně zjišťují hodnoty jednotlivých znaků (v případě jazyka Java se jedná o šestnáctibitová čísla) a hešovací kód se počítá pomocí iteračního vzorce h_new = h_old * 31 + hodnota_znaku. Za povšimnutí ovšem stojí dvě skutečnosti. Tou první je fakt, že se hešovací hodnota řetězce počítá skutečně jen z hodnot jednotlivých znaků a ostatní atributy objektu se ignorují (což je to, co programátor většinou požaduje, protože ho nezajímá, jaké další pro něj nepodstatné atributy objekty typu String obsahují). Druhá zajímavost spočívá v optimalizaci použité v metodě hashCode(). Namísto toho, aby se hešovací hodnota počítala stále znovu a znovu po každém volání metody hashCode(), je právě vypočtená hodnota uložena do atributu hash a výpočet hešovací hodnoty se provede pouze tehdy, pokud je hodnota tohoto atributu nulová. Připomeňme si, že řetězce jsou neměnitelné objekty, takže nikde není nutné zkoumat, zda došlo ke změně stavu objektu – jediné, co je nutné provést, je vynulování atributu hash v konstruktoru (hešovací kód se tak vypočítá jen tehdy, pokud je to skutečně nutné, například při ukládání řetězce do kolekce).
Obrázek 6: Implementace metody hashCode() ve třídě java.lang.String.
Vygenerování zdrojového textu metod equals() a hashCode()
Vzhledem k tomu, že při překrytí metody equals() je většinou nutné postupně porovnat hodnoty všech vybraných atributů, byla by ruční tvorba této metody v mnoha případech velmi zdlouhavá, a to zejména tehdy, pokud třída, pro niž se metoda equals() překrývá, obsahuje velký počet atributů. Nesmíme taktéž zapomenout na to, že mnoho atributů může být nastaveno na null, čehož lze využít pro urychlení porovnávání (nemá například smysl volat mnohdy časově náročnou metodu String.equals() s parametrem null). Ovšem vzhledem k tomu, že se jedná o relativně snadno algoritmizovatelnou činnost, může programátorům v této činnosti pomoci vývojové prostředí Eclipse, které dokáže metodu equals() vytvořit na základě výběru všech atributů, které se mají v equals() porovnávat – existují totiž případy, kdy je nutné některý atribut z porovnání vynechat. Podobné je to v případě metody hashCode(), v níž se postupně zjišťují hešovací hodnoty atributů a tyto hodnoty se posléze navzájem kombinují. Postup vytvoření vlastní varianty metod equals() a hashCode() je zobrazen na následující sekvenci screenshotů:
Obrázek 7: Metody equals() a hashCode() budeme vytvářet pro „hodnotový objekt“ nazvaný Person, jehož zdrojový kód jsme postupně tvořili v předchozí části tohoto seriálu. Tento objekt obsahuje trojici atributů, settery a gettery pro tyto atributy a taktéž dvojici konstruktorů – jeden z konstruktorů je přitom bezparametrický a druhý konstruktor nastavuje hodnoty všech atributů objektu.
Obrázek 8: Pro vytvoření metod equals() a hashCode() je nejdříve nutné vybrat z menu „Source“ příkaz „Generate hashCode() and equals()“.
Obrázek 9: Následně se zobrazí dialog, v němž je možné vybrat ty atributy objektu, které se použijí v tělech metod equals() a hashCode(). Atributy se vybírají pro obě zmíněné metody současně, protože jen tak je možné dodržet vlastnosti, které jsou po obou metodách vyžadované.
Obrázek 10: Na tomto snímku obrazovky je zobrazen zdrojový kód metody hashCode() vygenerované automaticky vývojovým prostředím Eclipse. Povšimněte si, že se nejdříve vypočítají hešovací kódy obou atributů typu String a posléze se tyto dvě hešovací hodnoty zkombinují s hodnotou celočíselného atributu age. Dále si povšimněte, že tato metoda nikdy nepovede ke vzniku výjimky NullPointerException, samozřejmě za předpokladu, že je metoda String.hashCode() vytvořena korektně. Postup, pomocí něhož bylo tělo této metody vygenerováno, je popsán například v knize Joshuy Blocha „Java Effective“.
Obrázek 11: Zdrojový kód metody equals() vygenerovaný integrovaným vývojovým prostředím Eclipse. Zajímavé je, že IDE Eclipse na začátek této metody zařadilo i původní porovnání dvou referencí objektů (viz též implementace této metody pro třídu Object), což samozřejmě dává smysl, protože objekty se stejnou referencí musí být totožné objekty. Dále se u všech atributů testuje, zda nemají hodnotu null, a teprve v případě, že tomu tak není, se volá metoda equals() i pro jednotlivé atributy (výjimku samozřejmě tvoří atributy primitivních datových typů, u nichž postačuje použít operátor ==, resp. při opačné podmínce !=).
Otestování vygenerovaných metod equals() a hashCode()
Dvojici metod equals() a hashCode() vygenerovanou pro třídu Person integrovaným vývojovým prostředím Eclipse můžeme velmi snadno otestovat a zjistit přitom, za jakých podmínek se liší výsledek výrazu x==y a x.equals(y) v případě, že objekty nazvané x a y jsou instancemi třídy Person. Nejprve ve třídě Main vytvoříme novou metodu nazvanou testEqualsAndHashCode(), která vypíše hodnotu výrazu x == y, x.equals(y) a hashCode(x) pro tři instance třídy Person. Přitom se může jednat o libovolné instance této třídy, tj. i o shodné objekty mající stejnou referenci (jejich atributy tedy budou na haldě ležet na stejných adresách). Tělo metody testEqualsAndHashCode() je zobrazeno na snímku číslo 12:
Obrázek 12: Metoda testEqualsAndHashCode(), kterou je možné zavolat pro tři libovolné instance třídy Person.
Na třináctém obrázku je ukázán způsob použití metody testEqualsAndHashCode(), která je zavolána pro tři objekty nazvané p1, p2 a p3. Při prvním zavolání této metody sice objekty p1 a p2 leží na jiném místě na haldě (jedná se o různé reference), a tudíž výraz p1 == p2 vrátí pravdivostní hodnotu false, ovšem vzhledem k tomu, že všechny tři atributy těchto objektů jsou navzájem shodné, bude výraz p1.equals(p2) a samozřejmě taktéž výraz p2.equals(p1) vracet hodnotu true. Podobně volání metody p1.hashCode() a p2.hashCode() vrátí v tomto případě stejné celé číslo. Při druhém volání metody testEqualsAndHashCode() jsou již atributy všech tří objektů p1, p2 a p3 odlišné, což se samozřejmě projeví na hodnotě výrazů typu px.equals(py). Zajímavé je třetí volání metody testEqualsAndHashCode(), protože v tomto případě reference p1 a p2 představují shodné objekty, tj. p1 a p2 obsahují shodnou referenci. To znamená, že všechny tři výrazy p1 == p2, p1.equals(p2) a p2.equals(p1) budou vracet stejnou hodnotu, podobně jako výraz p1.hashCode() a p2.hashCode().
Obrázek 13: Otestování vlastností operátoru == a metod equals() a hashCode() pro instance třídy Person.
Obrázek 14: Výsledek vypsaný metodou testEqualsAndHashCode() pro tři různé hodnoty objektů p1, p2 a p3.
Vzhledem k tomu, že metody equals() a hashCode() vygenerované integrovaným vývojovým prostředím Eclipse pro třídu Person odpovídají předpokládanému „kontraktu“ popsanému mj. i v dokumentaci třídy Object, je možné instance třídy Person bez obav uložit do kolekcí, například do seznamů (rozhraní List implementované třídami ArrayList a LinkedList), asociativních polí neboli hešovacích map (rozhraní Map implementované třídami HashMap a TreeMap) a především taktéž množin (rozhraní Set implementované třídami HashSet a TreeSet). Na následující čtveřici snímků obrazovky je ukázáno chování instancí třídy Person ukládaných do množin, konkrétně do HashSetu. Připomeňme si, že před uložením dalšího prvku do množiny se testuje, zda tento prvek v množině již neexistuje, a právě pro tento test se používá metoda equals() v součinnosti s metodou hashCode(). Na obrázku číslo 15 a 16 je do množiny uložena trojice navzájem odlišných instancí třídy Person, zatímco u obrázků 17 a 18 je použita pětice objektů, ovšem ve výsledné množině jsou uloženy pouze objekty tři – dva zbývající objekty totiž byly ekvivalentní k již uloženým objektům (ekvivalence se přitom zjišťuje právě pomocí equals() – viz též dokumentace k rozhraní Set).
Obrázek 15: Jednoduchý testovací příklad, v němž jsou do množiny vloženy tři instance třídy Person, přičemž tyto tři instance nejsou navzájem ekvivalentní.
Obrázek 16: Výsledek běhu testovacího příkladu, jehož kód byl zobrazen na snímku číslo 15 – všechny tři objekty byly skutečně uloženy do množiny a jsou následně vypsány (pořadí výpisu však obecně neodpovídá pořadí vložení prvků do množiny).
Obrázek 17: Druhá varianta testovacího příkladu: do množiny je ukládáno pět objektů, ovšem pouze tři z nich nejsou navzájem ekvivalentní – to se interně ve třídě HashSet testuje zavoláním metody Person.equals().
Obrázek 18: Výsledek běhu testovacího příkladu, jehož kód byl zobrazen na snímku číslo 17 – pouze tři objekty byly uloženy do množiny, protože zbylé dva objekty byly ekvivalentní s dříve uloženými objekty.
- Eclipse – integrované vývojové prostředí pro Javu i další programovací jazyky
- Využití Eclipse pro vývoj aplikací v programovacím jazyku Java
- Eclipse a programovací jazyk Java: poloautomatické opravy chyb a refaktoring zdrojových kódů
- Eclipse a programovací jazyk Java: automatická tvorba zdrojových kódů