Ta blog pogosto uporabljam za ponovni ogled težko prisluženih lekcij o osnovah Jave. Ta objava v blogu je en tak primer in se osredotoča na ponazoritev nevarne moči, ki stoji za metodama equals (Object) in hashCode (). Ne bom zajemal vseh odtenkov teh dveh zelo pomembnih metod, ki jih imajo vsi objekti Java, ne glede na to, ali so izrecno deklarirani ali implicitno podedovani od starša (morda neposredno od samega predmeta), vendar bom zajemal nekaj pogostih težav, ki se pojavijo, ko se ne izvajajo ali se ne izvajajo pravilno. S temi predstavitvami poskušam pokazati tudi, zakaj je za natančen pregled kode, temeljito testiranje enot in / ali analizo, ki temelji na orodjih, pomembno preveriti pravilnost izvedb teh metod.
Ker vsi predmeti Java na koncu podedujejo izvedbe za je enako (objekt)
in hashCode ()
, prevajalnik Java in izvajalnik Java Runtime zagotovo ne bo poročal o težavah pri klicu teh "privzetih izvedb" teh metod. Ko so te metode na žalost, so privzete izvedbe teh metod (na primer njihov bratranec metoda toString) le redko zaželene. Dokumentacija API-ja na osnovi Javadoc za razred Object obravnava "pogodbo", ki se pričakuje od kakršne koli izvedbe je enako (objekt)
in hashCode ()
metode in razpravlja tudi o verjetni privzeti izvedbi vsake, če je ne preglasijo podrejeni razredi.
Za primere v tej objavi bom uporabil razred HashAndEquals, katerega seznam kode je prikazan poleg procesnih primerkov objektov različnih razredov Person z različnimi stopnjami podpore za hashCode
in enako
metode.
HashAndEquals.java
paket prah v.primeri; uvoz java.util.HashSet; import java.util.Set; uvoz statičnega java.lang.System.out; javni razred HashAndEquals {zasebni statični končni niz HEADER_SEPARATOR = "======================================= =============================== ""; zasebni statični končni int HEADER_SEPARATOR_LENGTH = HEADER_SEPARATOR.length (); zasebni statični končni niz NEW_LINE = System.getProperty ("line.separator"); zasebna končna oseba person1 = nova oseba ("Flintstone", "Fred"); zasebna končna oseba person2 = nova oseba ("Rubble", "Barney"); zasebna končna oseba person3 = nova oseba ("Flintstone", "Fred"); zasebna končna oseba person4 = nova oseba ("Rubble", "Barney"); public void displayContents () {printHeader ("VSEBINA PREDMETOV"); out.println ("Oseba 1:" + oseba1); out.println ("Oseba 2:" + oseba2); out.println ("Oseba 3:" + oseba3); out.println ("Oseba 4:" + oseba4); } public void compareEquality () {printHeader ("PRIMERJAVE ENAKOSTI"); 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 ("PRIMERJAJ RAZMERJENE KODE"); 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 ("DODAJ ELEMENTE ZA NASTAVITEV - SO DODANI ALI ISTI?"); končni set set = nov 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)); povratni komplet; } public void removeFromHashSet (final Set sourceSet) {printHeader ("ODSTRANI ELEMENTE IZ NARAVE - ALI SO LAHKO NAJDENI ODSTRANITI?"); 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)); } javna statična void printHeader (končni niz headerText) {out.println (NEW_LINE); out.println (HEADER_SEPARATOR); out.println ("=" + headerText); out.println (HEADER_SEPARATOR); } public static void main (final String [] argument) {final HashAndEquals instance = new HashAndEquals (); instance.displayContents (); instance.compareEquality (); instance.compareHashCodes (); končni Set set = instance.addToHashSet (); out.println ("Set Before Removals:" + set); //instance.person1.setFirstName("Bam Bam "); instance.removeFromHashSet (set); out.println ("Set After Removals:" + set); }}
Zgornji razred bo večkrat uporabljen takšen, kot je, z le eno manjšo spremembo kasneje v objavi. Vendar pa Oseba
razred bo spremenjen, da bo odražal pomen enako
in hashCode
in pokazati, kako enostavno je lahko to zamočiti, hkrati pa je težko izslediti težavo, ko pride do napake.
Brez eksplicitnega enako
ali hashCode
Metode
Prva različica Oseba
razred ne vsebuje izrecno razveljavljene različice enako
metoda ali hashCode
metoda. To bo pokazalo "privzeto izvajanje" vsake od teh metod, podedovanih od Predmet
. Tu je izvorna koda za Oseba
brez hashCode
ali enako
izrecno razveljavljeno.
Person.java (brez eksplicitne hashCode ali enake metode)
paket prah v.primeri; javni razred Oseba {private final String lastName; zasebni končni niz firstName; javna oseba (končni niz newLastName, končni niz newFirstName) {this.lastName = newLastName; this.firstName = newFirstName; } @Override public String toString () {return this.firstName + "" + this.lastName; }}
Ta prva različica Oseba
ne zagotavlja metod get / set in ne zagotavlja enako
ali hashCode
izvedb. Ko je glavni predstavitveni razred HashAndEquals
se izvede s primerki tega enako
-brez in hashCode
-brez Oseba
razreda se rezultati prikažejo, kot je prikazano na naslednjem posnetku zaslona.
Iz zgoraj prikazanega rezultata je mogoče pridobiti več opažanj. Prvič, brez izrecne izvedbe je enako (objekt)
metoda, nobeden od primerov Oseba
se štejejo za enake, tudi če so vsi atributi primerkov (dva niza) enaki. Kot je razloženo v dokumentaciji za Object.equals (Object), je privzeta vrednost enako
izvedba temelji na natančnem referenčnem ujemanju:
Druga ugotovitev iz tega prvega primera je, da se hash koda razlikuje za vsak primerek Oseba
objekt, tudi če imata dva primerka enake vrednosti za vse svoje atribute. Vrne se HashSet prav
ko je naboru dodan "unikatni" objekt (HashSet.add) oz napačno
če se dodani predmet ne šteje za edinstvenega in zato ni dodan. Podobno je HashSet
vrne metoda odstranitve prav
če se predvideni predmet šteje za najdenega in odstranjenega oz napačno
če se šteje, da navedeni objekt ni del HashSet
in ga zato ni mogoče odstraniti. Zaradi enako
in hashCode
podedovane privzete metode te primere obravnavajo kot popolnoma drugačne, zato ni presenetljivo, da so vsi dodani v nabor in vsi uspešno odstranjeni iz nabora.
Izrecno enako
Samo metoda
Druga različica Oseba
razred vključuje izrecno razveljavljeno enako
metoda, kot je prikazano v naslednjem seznamu kod.
Person.java (na voljo je izrecna enaka metoda)
paket prah v.primeri; javni razred Oseba {private final String lastName; zasebni končni niz firstName; javna oseba (končni niz newLastName, končni niz 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; } končna oseba druga = (oseba) 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; } vrni true; } @Override public String toString () {return this.firstName + "" + this.lastName; }}
Ko primeri tega Oseba
s je enako (objekt)
uporabljeni izrecno, izhod je tak, kot je prikazano na naslednjem posnetku zaslona.
Prvo opažanje je, da zdaj enako
poziva Oseba
primeri se resnično vrnejo prav
kadar je objekt enak glede na to, da so vsi atributi enaki, namesto da bi preverjali, ali obstaja enaka referenca. To dokazuje, da je običaj enako
izvajanje na Oseba
je opravil svoje delo. Druga ugotovitev je, da je izvajanje enako
metoda ni vplivala na zmožnost dodajanja in odstranjevanja na videz istega predmeta v datoteko HashSet
.
Izrecno enako
in hashCode
Metode
Zdaj je čas, da dodate eksplicitno hashCode ()
metoda do Oseba
razred. Dejansko bi bilo to res treba storiti, ko enako
metoda je bila izvedena. Razlog za to je naveden v dokumentaciji za Object.equals (Object)
metoda:
Tukaj je Oseba
z izrecno izvedenim hashCode
metoda, ki temelji na istih lastnostih Oseba
kot enako
metoda.
Person.java (eksplicitne enake in izvedbe hashCode)
paket prah v.primeri; javni razred Oseba {private final String lastName; zasebni končni niz firstName; javna oseba (končni niz newLastName, končni niz 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; } končna oseba druga = (oseba) 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; } vrni true; } @Override public String toString () {return this.firstName + "" + this.lastName; }}
Rezultat izvajanja z novim Oseba
razred s hashCode
in enako
naslednje metode.
Ni presenetljivo, da so hash kode, ki se vrnejo za predmete z enakimi vrednostmi atributov, zdaj enake, bolj zanimivo pa je opazovanje, da lahko dodamo le dva od štirih primerkov HashSet
zdaj. Razlog za to je, da se pri tretjem in četrtem poskusu dodajanja poskuša dodati objekt, ki je bil že dodan v nabor. Ker sta bila dodana samo dva, je mogoče najti in odstraniti le dva.
Težave z spremenljivimi atributi hashCode
Za četrti in zadnji primer v tej objavi pogledam, kaj se zgodi, ko hashCode
izvedba temelji na atributu, ki se spremeni. V tem primeru: a setFirstName
je dodana metoda Oseba
in dokončno
modifikator je odstranjen iz ime
atribut. Poleg tega mora glavni razred HashAndEquals odstraniti komentar iz vrstice, ki prikliče to novo nastavljeno metodo. Nova različica Oseba
je prikazan naslednji.
paket prah v.primeri; javni razred Oseba {private final String lastName; private String firstName; javna oseba (končni niz newLastName, končni niz newFirstName) {this.lastName = newLastName; this.firstName = newFirstName; } @Override public int hashCode () {return lastName.hashCode () + firstName.hashCode (); } javna praznina setFirstName (končni niz 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; } končna oseba druga = (oseba) 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; } vrni true; } @Override public String toString () {return this.firstName + "" + this.lastName; }}
Izhod, ustvarjen z izvajanjem tega primera, je prikazan naprej.