Programovanie

Základný hashCode jazyka Java a rovná sa Demonštrácie

Tento blog často rád používam na to, aby som sa vrátil k vyťaženým lekciám základov Java. Jedným z príkladov je tento blogový príspevok, ktorý sa zameriava na ilustráciu nebezpečnej sily, ktorá stojí za metódami equals (Object) a hashCode (). Nebudem sa venovať každej nuancii týchto dvoch veľmi významných metód, ktoré majú všetky objekty Java, či už sú výslovne deklarované alebo implicitne zdedené od rodiča (možno priamo od samotného objektu), ale budem sa venovať niektorým bežným problémom, ktoré vznikajú, keď sú nie sú implementované alebo nie sú implementované správne. Pokúšam sa tiež týmito ukážkami ukázať, prečo je dôležité overiť správnosť implementácie týchto metód pri dôkladných kontrolách kódu, dôkladnom testovaní jednotiek alebo analýze na základe nástrojov.

Pretože všetky objekty Java nakoniec dedia implementácie pre rovná sa (Objekt) a hashCode (), kompilátor Java a spúšťač runtime Java nehlásia žiadny problém pri vyvolaní týchto „predvolených implementácií“ týchto metód. Bohužiaľ, keď sú tieto metódy potrebné, predvolené implementácie týchto metód (napríklad ich bratranec metóda toString) sú zriedka požadované. Dokumentácia API založená na Javadocu pre triedu Object pojednáva o „zmluve“ očakávanej od akejkoľvek implementácie rovná sa (Objekt) a hashCode () metódy a tiež pojednáva o pravdepodobnej predvolenej implementácii každej, ak nie je prepísaná podradenými triedami.

Pre príklady v tomto príspevku budem používať triedu HashAndEquals, ktorej výpis kódu sa zobrazuje vedľa procesu inštancií objektov rôznych tried osôb s rôznymi úrovňami podpory pre hashCode a rovná sa metódy.

HashAndEquals.java

balenie zásypu.príklady; import java.util.HashSet; import java.util.Set; importovať statický java.lang.System.out; verejná trieda 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"); súkromná posledná osoba person1 = nová osoba ("Flintstone", "Fred"); súkromná konečná osoba person2 = nová osoba ("Suť", "Barney"); súkromná posledná osoba person3 = nová osoba ("Flintstone", "Fred"); súkromná posledná osoba person4 = nová osoba ("Suť", "Barney"); public void displayContents () {printHeader ("OBSAH OBJEKTOV"); out.println ("Osoba 1:" + osoba1); out.println ("Osoba 2:" + osoba2); out.println ("Osoba 3:" + osoba3); out.println ("Osoba 4:" + osoba4); } public void compareEquality () {printHeader ("POROVNANIE 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 ("POROVNÁVAŤ HASHOVÉ 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 ("PRIDAŤ PRVKY DO SADY - SÚ PRIDANÉ ALEBO ROVNAKÉ?"); posledná sada množín = nová HashSet (); out.println ("Set.add (Osoba1):" + set.add (osoba1)); 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á súprava; } public void removeFromHashSet (final Set sourceSet) {printHeader ("ODSTRÁNIŤ PRVKY ZO SADY - MOŽNO ZISTIŤ, ŽE SA MAJÚ ODSTRÁNIŤ?"); 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 argumenty String []) {final HashAndEquals instance = new HashAndEquals (); instance.displayContents (); instance.compareEquality (); instance.compareHashCodes (); final Set set = instance.addToHashSet (); out.println ("Nastaviť pred odstránením:" + sada); //instance.person1.setFirstName("Bam Bam "); instance.removeFromHashSet (set); out.println ("Sada po odstránení:" + sada); }} 

Vyššie uvedená trieda sa bude opakovane používať tak, ako je, len s jednou ďalšou zmenou v príspevku. Avšak Osoba triedy sa zmení tak, aby odrážala dôležitosť triedy rovná sa a hashCode a demonštrovať, ako ľahko je možné ich pokaziť a zároveň je ťažké problém v prípade chyby zistiť.

Žiadne výslovné rovná sa alebo hashCode Metódy

Prvá verzia Osoba trieda neposkytuje explicitnú prepísanú verziu ani jedného z nich rovná sa metóda alebo hashCode metóda. Toto demonštruje „predvolenú implementáciu“ každej z týchto metód zdedenú od Objekt. Tu je zdrojový kód pre Osoba bez hashCode alebo rovná sa vyslovene prepísané.

Person.java (žiadna explicitná metóda hashCode alebo metóda equals)

balenie zásypu.príklady; verejná trieda Osoba {private final String priezvisko; súkromné ​​konečné reťazec meno; public Person (konečný reťazec newLastName, konečný reťazec newFirstName) {this.lastName = newLastName; this.firstName = newFirstName; } @Override public String toString () {return this.firstName + "" + this.lastName; }} 

Táto prvá verzia Osoba neposkytuje metódy get / set a neposkytuje rovná sa alebo hashCode implementácie. Keď hlavná ukážková trieda HashAndEquals sa vykonáva s prípadmi tohto rovná sa-less a hashCode-less Osoba triedy sa výsledky zobrazia tak, ako je to znázornené na nasledujúcej snímke obrazovky.

Z výstupu zobrazeného vyššie je možné urobiť niekoľko pozorovaní. Po prvé, bez výslovnej implementácie rovná sa (Objekt) metóda, žiadny z prípadov Osoba sa považujú za rovnocenné, aj keď sú všetky atribúty inštancií (dva reťazce) rovnaké. Je to preto, lebo ako je vysvetlené v dokumentácii k Object.equals (Object), predvolená hodnota je rovná sa implementácia je založená na presnej referenčnej zhode:

Metóda equals pre triedu Object implementuje najdiskriminačnejší možný vzťah ekvivalencie na objektoch; to znamená, že pre akékoľvek nenulové referenčné hodnoty x a y táto metóda vráti hodnotu true vtedy a len vtedy, ak x a y odkazujú na rovnaký objekt (x == y má hodnotu true).

Druhým zistením z tohto prvého príkladu je, že hashový kód je pre každú inštanciu súboru odlišný Osoba objekt, aj keď dve inštancie zdieľajú rovnaké hodnoty pre všetky svoje atribúty. HashSet sa vráti pravda keď je do sady pridaný "jedinečný" objekt (HashSet.add) alebo nepravdivé ak sa pridaný objekt nepovažuje za jedinečný a tak sa nepridá. Podobne HashSetmetóda odstránenia sa vráti pravda ak sa poskytnutý objekt považuje za nájdený a odstránený alebo nepravdivé ak sa uvedený objekt nepovažuje za súčasť HashSet a tak sa nedá odstrániť. Pretože rovná sa a hashCode zdedené predvolené metódy považujú tieto inštancie za úplne odlišné, nie je prekvapením, že všetky sú pridané do množiny a všetky sú z množiny úspešne odstránené.

Výslovné rovná sa Iba metóda

Druhá verzia Osoba trieda obsahuje výslovne prepísané rovná sa ako je uvedené v nasledujúcom zozname kódov.

Person.java (uvedená metóda explicitného rovná sa)

balenie zásypu.príklady; verejná trieda Osoba {private final String priezvisko; súkromné ​​konečné reťazec meno; public Person (konečný reťazec newLastName, konečný reťazec 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 iná = (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 pravdivý; } @Override public String toString () {return this.firstName + "" + this.lastName; }} 

Keď prípady tohto Osoba s rovná sa (Objekt) použijú sa výslovne definované, výstup je uvedený na nasledujúcej snímke obrazovky.

Prvým zistením je, že teraz rovná sa vyzýva Osoba prípady sa skutočne vrátia pravda keď je objekt rovnaký, pokiaľ ide o všetky atribúty, ktoré sú rovnaké, namiesto kontroly prísnej referenčnej rovnosti. To ukazuje, že zvyk rovná sa implementácia dňa Osoba urobil svoju prácu. Druhým zistením je, že implementácia rovná sa Táto metóda nemala žiadny vplyv na schopnosť pridávať a odstraňovať zdanlivo rovnaký objekt do súboru HashSet.

Výslovné rovná sa a hashCode Metódy

Teraz je čas pridať výslovné hashCode () metóda do Osoba trieda. Toto sa skutočne malo urobiť, keď rovná sa bola implementovaná metóda. Dôvod je uvedený v dokumentácii k Object.equals (Objekt) metóda:

Všimnite si, že je všeobecne potrebné prepísať metódu hashCode vždy, keď je táto metóda prepísaná, aby sa zachoval všeobecný kontrakt pre metódu hashCode, ktorý uvádza, že rovnaké objekty musia mať rovnaké hašovacie kódy.

Tu je Osoba s výslovne implementovaným hashCode metóda založená na rovnakých atribútoch Osoba ako rovná sa metóda.

Person.java (explicitné implementácie rovná sa a hashCode)

balenie zásypu.príklady; verejná trieda Osoba {private final String priezvisko; súkromné ​​konečné reťazec meno; public Person (konečný reťazec newLastName, konečný reťazec 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 iná = (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 pravdivý; } @Override public String toString () {return this.firstName + "" + this.lastName; }} 

Výstup z chodu s novým Osoba trieda s hashCode a rovná sa nasledujúce metódy.

Nie je prekvapením, že hašovacie kódy vrátené pre objekty s rovnakými hodnotami atribútov sú teraz rovnaké, ale zaujímavejšie je zistenie, že do súboru môžeme pridať iba dva zo štyroch inštancií. HashSet teraz. Je to tak preto, lebo tretí a štvrtý pokus o pridanie sa považujú za pokus o pridanie objektu, ktorý už bol do množiny pridaný. Pretože boli pridané iba dva, je možné nájsť a odstrániť iba dva.

Problém s premenlivými atribútmi hashCode

Ako štvrtý a posledný príklad v tomto príspevku sa pozriem na to, čo sa stane, keď hashCode implementácia je založená na atribúte, ktorý sa mení. Pre tento príklad a setFirstName metóda je pridaná k Osoba a konečné modifikátor je odstránený z jeho krstné meno atribút. Okrem toho musí byť v hlavnej triede HashAndEquals odstránený komentár z riadku, ktorý vyvoláva túto novú metódu množiny. Nová verzia Osoba sa zobrazí ďalej.

balenie zásypu.príklady; verejná trieda Osoba {private final String priezvisko; private String meno; public Person (konečný reťazec newLastName, konečný reťazec newFirstName) {this.lastName = newLastName; this.firstName = newFirstName; } @Override public int hashCode () {return lastName.hashCode () + firstName.hashCode (); } public void setFirstName (konečný reťazec 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 iná = (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 pravdivý; } @Override public String toString () {return this.firstName + "" + this.lastName; }} 

Ďalej sa zobrazí výstup vygenerovaný spustením tohto príkladu.

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