Č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ě:
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ě HashSet
vrací 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:
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.