Programování

Základní Java hashCode a rovná se Demonstrace

Často rád používám tento blog k návštěvě těžce vydělaných lekcí základů Java. Tento příspěvek na blogu je jedním z takových příkladů a zaměřuje se na ilustraci nebezpečné síly za metodami equals (Object) a hashCode (). Nebudu pokrýt všechny nuance těchto dvou vysoce významných metod, které mají všechny objekty Java, ať už výslovně deklarované nebo implicitně zděděné od rodiče (možná přímo od samotného objektu), ale budu se zabývat některými běžnými problémy, které se vyskytnou, když nejsou implementovány nebo nejsou implementovány správně. Pokusím se také ukázat na těchto demonstracích, proč je důležité ověřit správnost implementace těchto metod pečlivými kontrolami kódu, důkladným testováním jednotek nebo analýzou pomocí nástrojů.

Protože všechny objekty Java nakonec dědí implementace pro rovná se (Objekt) a hashCode (), kompilátor Java a spouštěcí modul Java runtime nehlásí žádný problém při vyvolání těchto „výchozích implementací“ těchto metod. Bohužel, když jsou tyto metody potřeba, jsou výchozí implementace těchto metod (jako jejich bratranec metoda toString) zřídka požadované. Dokumentace API založená na Javadocu pro třídu Object pojednává o „smlouvě“ očekávané od jakékoli implementace rovná se (Objekt) a hashCode () metody a také popisuje pravděpodobnou výchozí implementaci každé, pokud není přepsána podřízenými třídami.

U příkladů v tomto příspěvku budu používat třídu HashAndEquals, jejíž výpis kódu je zobrazen vedle zpracování instancí objektů různých Person tříd s rozdílnou úrovní podpory pro hashCode a se rovná metody.

HashAndEquals.java

příklady zásilky; import java.util.HashSet; import java.util.Set; importovat statický java.lang.System.out; veřejná třída HashAndEquals {private static final String HEADER_SEPARATOR = "======================================= ================================ "; private static final int HEADER_SEPARATOR_LENGTH = HEADER_SEPARATOR.length (); private static final String NEW_LINE = System.getProperty ("line.separator"); soukromá konečná osoba person1 = nová osoba ("Flintstone", "Fred"); soukromá konečná osoba person2 = nová osoba ("Rubble", "Barney"); soukromá konečná osoba person3 = nová Osoba ("Flintstone", "Fred"); soukromá konečná osoba person4 = nová osoba ("Rubble", "Barney"); public void displayContents () {printHeader ("OBSAH OBJEKTŮ"); out.println ("Osoba 1:" + osoba1); out.println ("Osoba 2:" + osoba2); out.println ("Osoba 3:" + osoba3); out.println ("Osoba 4:" + osoba4); } public void compareEquality () {printHeader ("POROVNÁNÍ ROVNOSTI"); out.println ("Person1.equals (Person2):" + person1.equals (person2)); out.println ("Person1.equals (Person3):" + person1.equals (person3)); out.println ("Person2.equals (Person4):" + person2.equals (person4)); } public void compareHashCodes () {printHeader ("POROVNAT HASH KÓDY"); out.println ("Person1.hashCode ():" + person1.hashCode ()); out.println ("Person2.hashCode ():" + person2.hashCode ()); out.println ("Person3.hashCode ():" + person3.hashCode ()); out.println ("Person4.hashCode ():" + person4.hashCode ()); } public Set addToHashSet () {printHeader ("PŘIDAT PRVKY DO SADY - JSOU PŘIDÁNY NEBO STEJNÉ?"); final Set set = new HashSet (); out.println ("Set.add (Person1):" + set.add (person1)); out.println ("Set.add (Person2):" + set.add (person2)); out.println ("Set.add (Person3):" + set.add (person3)); out.println ("Set.add (Person4):" + set.add (person4)); návratová sada; } public void removeFromHashSet (final Set sourceSet) {printHeader ("REMOVE ELEMENTS FROM SET - CAN ON THE FOUND TO BE REMOVED?"); out.println ("Set.remove (Person1):" + sourceSet.remove (person1)); out.println ("Set.remove (Person2):" + sourceSet.remove (person2)); out.println ("Set.remove (Person3):" + sourceSet.remove (person3)); out.println ("Set.remove (Person4):" + sourceSet.remove (person4)); } public static void printHeader (final String headerText) {out.println (NEW_LINE); out.println (HEADER_SEPARATOR); out.println ("=" + headerText); out.println (HEADER_SEPARATOR); } public static void main (final String [] argumenty) {final HashAndEquals instance = new HashAndEquals (); instance.displayContents (); instance.compareEquality (); instance.compareHashCodes (); final Set set = instance.addToHashSet (); out.println ("Set Before Removals:" + set); //instance.person1.setFirstName("Bam Bam "); instance.removeFromHashSet (sada); out.println ("Set After Removals:" + set); }} 

Třída výše bude opakovaně použita tak, jak je, pouze s jednou menší změnou později v příspěvku. Nicméně Osoba třída bude změněna tak, aby odrážela důležitost se rovná a hashCode a ukázat, jak snadno je možné je pokazit a zároveň je obtížné problém v případě chyby vypátrat.

Žádné výslovné se rovná nebo hashCode Metody

První verze Osoba třída neposkytuje explicitní přepsanou verzi buď se rovná metoda nebo hashCode metoda. To předvede „výchozí implementaci“ každé z těchto metod zděděných od Objekt. Zde je zdrojový kód pro Osoba bez hashCode nebo se rovná výslovně přepsán.

Person.java (žádná explicitní metoda hashCode nebo metoda equals)

příklady zásilky; veřejná třída Osoba {private final String lastName; private final Řetězec firstName; public Person (final String newLastName, final String newFirstName) {this.lastName = newLastName; this.firstName = newFirstName; } @Override public String toString () {return this.firstName + "" + this.lastName; }} 

Tato první verze Osoba neposkytuje metody get / set a neposkytuje se rovná nebo hashCode implementace. Když hlavní demonstrační třída HashAndEquals se provádí s instancemi tohoto se rovná-less a hashCode-méně Osoba třídy, výsledky se zobrazí, jak je znázorněno na následujícím snímku obrazovky.

Z výše uvedeného výstupu lze provést několik pozorování. Nejprve bez výslovné implementace rovná se (Objekt) metoda, žádná z instancí Osoba jsou považovány za rovnocenné, i když jsou všechny atributy instancí (dva řetězce) stejné. Důvodem je, jak je vysvětleno v dokumentaci k Object.equals (Object), výchozí se rovná implementace je založena na přesné referenční shodě:

Metoda equals pro třídu Object implementuje co nejvíce diskriminační možný vztah ekvivalence na objekty; to znamená, že pro všechny nenulové referenční hodnoty xay tato metoda vrací true právě tehdy, když xay odkazují na stejný objekt (x == y má hodnotu true).

Druhým pozorováním z tohoto prvního příkladu je, že hash kód je pro každou instanci souboru odlišný Osoba objekt, i když dvě instance sdílejí stejné hodnoty pro všechny své atributy. HashSet se vrátí skutečný když je do sady přidán "jedinečný" objekt (HashSet.add) nebo Nepravdivé pokud přidaný objekt není považován za jedinečný, a tak není přidán. Podobně HashSetvrací metodu remove skutečný pokud je poskytnutý objekt považován za nalezený a odstraněný nebo Nepravdivé pokud je zadaný objekt považován za součást HashSet a proto jej nelze odstranit. Protože se rovná a hashCode zděděné výchozí metody považují tyto instance za zcela odlišné, není žádným překvapením, že jsou všechny přidány do sady a všechny jsou ze sady úspěšně odebrány.

Výslovný se rovná Pouze metoda

Druhá verze Osoba třída obsahuje výslovně přepsané se rovná metoda, jak je uvedeno v následujícím seznamu kódů.

Person.java (je k dispozici metoda explicitní rovnosti)

příklady zásilky; veřejná třída Osoba {private final String lastName; private final Řetězec firstName; public Person (final String newLastName, final String newFirstName) {this.lastName = newLastName; this.firstName = newFirstName; } @Override public boolean equals (Object obj) {if (obj == null) {return false; } if (this == obj) {return true; } if (this.getClass ()! = obj.getClass ()) {return false; } konečná osoba jiná = (osoba) obj; if (this.lastName == null? other.lastName! = null:! this.lastName.equals (other.lastName)) {return false; } if (this.firstName == null? other.firstName! = null:! this.firstName.equals (other.firstName)) {return false; } návrat true; } @Override public String toString () {return this.firstName + "" + this.lastName; }} 

Když instance tohoto Osoba s rovná se (Objekt) jsou použity explicitně definované, výstup je zobrazen na následujícím snímku obrazovky.

První pozorování je, že nyní se rovná vyzývá Osoba instance se skutečně vrátí skutečný když je objekt stejný, pokud jde o všechny atributy, které jsou stejné, spíše než kontrola přísné referenční rovnosti. To ukazuje, že zvyk se rovná implementace dne Osoba odvedl svou práci. Druhým postřehem je, že provádění se rovná metoda neměla žádný vliv na schopnost přidat a odebrat zdánlivě stejný objekt do HashSet.

Výslovný se rovná a hashCode Metody

Nyní je čas přidat explicitní hashCode () metoda k Osoba třída. Opravdu to mělo být provedeno, když se rovná byla implementována metoda. Důvod je uveden v dokumentaci k Object.equals (Object) metoda:

Všimněte si, že je obecně nutné přepsat metodu hashCode, kdykoli je tato metoda přepsána, aby se zachovala obecná smlouva pro metodu hashCode, která uvádí, že stejné objekty musí mít stejné hash kódy.

Tady je Osoba s výslovně implementován hashCode metoda založená na stejných atributech Osoba jako se rovná metoda.

Person.java (explicitní rovná se a implementace hashCode)

příklady zásilky; veřejná třída Osoba {private final String lastName; private final Řetězec firstName; public Person (final String newLastName, final String newFirstName) {this.lastName = newLastName; this.firstName = newFirstName; } @Override public int hashCode () {return lastName.hashCode () + firstName.hashCode (); } @Override public boolean equals (Object obj) {if (obj == null) {return false; } if (this == obj) {return true; } if (this.getClass ()! = obj.getClass ()) {return false; } konečná osoba jiná = (osoba) obj; if (this.lastName == null? other.lastName! = null:! this.lastName.equals (other.lastName)) {return false; } if (this.firstName == null? other.firstName! = null:! this.firstName.equals (other.firstName)) {return false; } návrat true; } @Override public String toString () {return this.firstName + "" + this.lastName; }} 

Výstup z běhu s novým Osoba třída s hashCode a se rovná metody jsou zobrazeny dále.

Není divu, že hash kódy vrácené pro objekty se stejnými hodnotami atributů jsou nyní stejné, ale zajímavější pozorování je, že můžeme přidat pouze dvě ze čtyř instancí do HashSet Nyní. Důvodem je, že třetí a čtvrtý pokus o přidání se považují za pokus o přidání objektu, který již byl do sady přidán. Protože byly přidány pouze dva, lze najít a odebrat pouze dva.

Potíže s proměnlivými atributy hashCode

U čtvrtého a posledního příkladu v tomto příspěvku se podívám na to, co se stane, když hashCode implementace je založena na atributu, který se mění. V tomto příkladu a setFirstName metoda je přidána do Osoba a finále modifikátor je odstraněn z jeho jméno atribut. Kromě toho hlavní třída HashAndEquals musí mít komentář odstraněn z řádku, který vyvolá tuto novou metodu sady. Nová verze Osoba se zobrazí dále.

příklady zásilky; veřejná třída Osoba {private final String lastName; private String firstName; public Person (final String newLastName, final String newFirstName) {this.lastName = newLastName; this.firstName = newFirstName; } @Override public int hashCode () {return lastName.hashCode () + firstName.hashCode (); } public void setFirstName (final String newFirstName) {this.firstName = newFirstName; } @Override public boolean equals (Object obj) {if (obj == null) {return false; } if (this == obj) {return true; } if (this.getClass ()! = obj.getClass ()) {return false; } konečná osoba jiná = (osoba) obj; if (this.lastName == null? other.lastName! = null:! this.lastName.equals (other.lastName)) {return false; } if (this.firstName == null? other.firstName! = null:! this.firstName.equals (other.firstName)) {return false; } návrat true; } @Override public String toString () {return this.firstName + "" + this.lastName; }} 

Výstup generovaný spuštěním tohoto příkladu je zobrazen dále.

$config[zx-auto] not found$config[zx-overlay] not found