equals()
és hashCode()
. Nem először találkozunk velük: a CodeGym tanfolyam egy rövid leckével kezdődik equals()
: olvasd el, ha elfelejtetted, vagy még nem láttad... A mai leckében arról lesz szó, ezeket a fogalmakat részletesen. És hidd el, van miről beszélnünk! De mielőtt rátérnénk az újra, frissítsük fel a már leírtakat :) Mint emlékszel, általában rossz ötlet két objektumot összehasonlítani az operátorral ==
, mert ==
összehasonlítja a hivatkozásokat. Íme a példánk az autókkal egy közelmúltbeli leckéből:
public class Car {
String model;
int maxSpeed;
public static void main(String[] args) {
Car car1 = new Car();
car1.model = "Ferrari";
car1.maxSpeed = 300;
Car car2 = new Car();
car2.model = "Ferrari";
car2.maxSpeed = 300;
System.out.println(car1 == car2);
}
}
Konzol kimenet:
false
Úgy tűnik, két azonos objektumot hoztunk létre Car
: a két autóobjektum megfelelő mezőinek értékei megegyeznek, de az összehasonlítás eredménye így is hamis. Az okot már tudjuk: a car1
és car2
hivatkozások különböző memóriacímekre mutatnak, tehát nem egyenlőek. De továbbra is a két objektumot szeretnénk összehasonlítani, nem két hivatkozást. Az objektumok összehasonlítására a legjobb megoldás a equals()
módszer.
egyenlő() metódus
Emlékezhet, hogy ezt a metódust nem a semmiből hozzuk létre, hanem felülírjuk: aequals()
metódus az osztályban van definiálva Object
. Ez azt jelenti, hogy a szokásos formájában nem sok haszna van:
public boolean equals(Object obj) {
return (this == obj);
}
Így van equals()
definiálva a metódus az osztályban Object
. Ez ismét a hivatkozások összehasonlítása. Miért csinálták így? Nos, honnan tudják a nyelv készítői, hogy a programodban mely objektumok számítanak egyenlőnek és melyek nem? :) Ez a metódus lényege equals()
– az osztály létrehozója az, aki meghatározza, hogy mely jellemzőket használjuk az osztály objektumai egyenlőségének ellenőrzésekor. Ezután felülírja a equals()
módszert az osztályában. Ha nem egészen érti a „meghatározza, mely jellemzőket” jelentését, nézzünk egy példát. Íme egy egyszerű osztály, amely egy férfit ábrázol: Man
.
public class Man {
private String noseSize;
private String eyesColor;
private String haircut;
private boolean scars;
private int dnaCode;
public Man(String noseSize, String eyesColor, String haircut, boolean scars, int dnaCode) {
this.noseSize = noseSize;
this.eyesColor = eyesColor;
this.haircut = haircut;
this.scars = scars;
this.dnaCode = dnaCode;
}
// Getters, setters, etc.
}
Tegyük fel, hogy írunk egy programot, aminek meg kell határoznia, hogy két ember egypetéjű ikrek-e, vagy egyszerűen hasonlatosak. Öt jellemzőnk van: orrméret, szemszín, frizura, hegek jelenléte és DNS-teszt eredményei (az egyszerűség kedvéért egész kódként ábrázoljuk). Ön szerint ezek közül melyik tulajdonság teszi lehetővé, hogy programunk azonosítsa az egypetéjű ikreket? Természetesen csak a DNS-vizsgálat adhat garanciát. Két embernek ugyanaz a szeme színe, a frizurája, az orra és még a sebhelyei is lehetnek – nagyon sok ember él a világon, és lehetetlen garantálni, hogy nincsenek kételyek. De szükségünk van egy megbízható mechanizmusra: csak a DNS-teszt eredménye alapján vonhatunk le pontos következtetést. Mit jelent ez a mi módszerünkre nézve equals()
? Felül kell írnunk aMan
osztályban, figyelembe véve a programunk követelményeit. A módszernek össze kell hasonlítania int dnaCode
a két objektum mezőjét. Ha egyenlőek, akkor a tárgyak egyenlőek.
@Override
public boolean equals(Object o) {
Man man = (Man) o;
return dnaCode == man.dnaCode;
}
Tényleg ilyen egyszerű? Nem igazán. Valamit figyelmen kívül hagytunk. Objektumaink esetében csak egy olyan mezőt azonosítottunk, amely releváns az objektum egyenlőség megállapításához: dnaCode
. Most képzeljük el, hogy nem 1, hanem 50 releváns mezőnk van. És ha két objektum mind az 50 mezője egyenlő, akkor az objektumok egyenlőek. Egy ilyen forgatókönyv is lehetséges. A fő probléma az, hogy az egyenlőség megállapítása 50 mező összehasonlításával idő- és erőforrás-igényes folyamat. Most képzeljük el, hogy az osztályunkon kívül Man
van egy Woman
osztályunk is, amely pontosan ugyanazokkal a mezőkkel rendelkezik, mint a Man
. Ha egy másik programozó használja az osztályainkat, könnyen írhat ilyen kódot:
public static void main(String[] args) {
Man man = new Man(........); // A bunch of parameters in the constructor
Woman woman = new Woman(.........); // The same bunch of parameters.
System.out.println(man.equals(woman));
}
Ebben az esetben a mezőértékek ellenőrzése értelmetlen: jól láthatjuk, hogy két különböző osztályú objektumunk van, tehát semmiképpen sem lehetnek egyenlőek! Ez azt jelenti, hogy a metódushoz egy ellenőrzést kell hozzáadnunk equals()
, amely összehasonlítja az összehasonlított objektumok osztályait. Jó, hogy erre gondoltunk!
@Override
public boolean equals(Object o) {
if (getClass() != o.getClass()) return false;
Man man = (Man) o;
return dnaCode == man.dnaCode;
}
De lehet, hogy mást is elfelejtettünk? Hmm... Legalább ellenőrizni kell, hogy nem hasonlítunk össze egy tárgyat önmagával! Ha az A és B hivatkozások ugyanarra a memóriacímre mutatnak, akkor ugyanaz az objektum, és nem kell időt vesztegetnünk 50 mező összehasonlítására.
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (getClass() != o.getClass()) return false;
Man man = (Man) o;
return dnaCode == man.dnaCode;
}
Az sem árt, ha egy csekket adunk hozzá null
: egyetlen objektum sem lehet egyenlő a -val null
. Tehát, ha a metódus paramétere null, akkor nincs értelme a további ellenőrzéseknek. Mindezt szem előtt tartva equals()
az Man
osztályra vonatkozó módszerünk így néz ki:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Man man = (Man) o;
return dnaCode == man.dnaCode;
}
Elvégezzük az összes fent említett kezdeti ellenőrzést. A nap végén, ha:
- két azonos osztályba tartozó objektumot hasonlítunk össze
- és az összehasonlított objektumok nem ugyanazok az objektumok
- és az átadott objektum nem
null
dnaCode
a két objektum mezőit jelenti. A módszer felülbírálásakor equals()
ügyeljen az alábbi követelmények betartására:
-
Reflexivitás.
Ha a
equals()
metódust arra használjuk, hogy bármely objektumot önmagával hasonlítson össze, akkor igazat kell adnia.
Ennek a követelménynek már eleget tettünk. Módszerünk a következőket tartalmazza:if (this == o) return true;
-
Szimmetria.
Ha
a.equals(b) == true
, akkorb.equals(a)
vissza kell térnietrue
.
Módszerünk ennek a követelménynek is megfelel. -
Tranzitivitás.
Ha két objektum egyenlő valamelyik harmadik tárggyal, akkor egyenlőnek kell lenniük egymással.
Haa.equals(b) == true
ésa.equals(c) == true
, akkorb.equals(c)
is igazat kell adnia. -
Kitartás.
A eredménynek
equals()
csak akkor kell változnia, ha az érintett mezők megváltoznak. Ha a két objektum adatai nem változnak, akkor az eredménynekequals()
mindig azonosnak kell lennie. -
Egyenlőtlenség -val
null
.Minden objektum esetén
a.equals(null)
hamis értéket kell visszaadnia
. Ez nem csak néhány "hasznos ajánlás", hanem egy szigorú szerződés , amelyet az Oracle dokumentációja tartalmaz.
hashCode() metódus
Most beszéljünk a módszerrőlhashCode()
. Miért van rá szükség? Pontosan ugyanerre a célra - tárgyak összehasonlítására. De már megvan equals()
! Miért más módszer? A válasz egyszerű: a teljesítmény javítása. A Java nyelvben a hashCode()
metódussal ábrázolt hash függvény fix hosszúságú számértéket ad vissza bármely objektumhoz. Java nyelven a metódus bármely objektumhoz hashCode()
32 bites számot ( ) ad vissza . int
Két szám összehasonlítása sokkal gyorsabb, mint két objektum összehasonlítása a equals()
módszerrel, különösen, ha ez a módszer sok mezőt vesz figyelembe. Ha a programunk objektumokat hasonlít össze, akkor ezt sokkal egyszerűbb megtenni hash kóddal. hashCode()
Az összehasonlítás csak akkor folytatódik, ha a metódus alapján az objektumok egyenlőekequals()
módszer. Egyébként így működnek a hash alapú adatstruktúrák, például az ismerős HashMap
! A hashCode()
metódust a metódushoz hasonlóan equals()
a fejlesztő felülírja. equals()
A módszerhez hasonlóan hashCode()
az Oracle dokumentációjában meghatározott hivatalos követelmények vannak:
-
Ha két objektum egyenlő (vagyis a
equals()
metódus true értéket ad vissza), akkor azonos hash kóddal kell rendelkezniük.Ellenkező esetben a módszereink értelmetlenek lennének. Ahogy fentebb említettük,
hashCode()
a teljesítmény javítása érdekében először ellenőrizni kell. Ha a hash kódok eltérőek lennének, akkor az ellenőrzés hamis értéket adna vissza, még akkor is, ha az objektumok valójában megegyeznek a metódus meghatározása szerintequals()
. -
Ha a
hashCode()
metódust többször hívják ugyanazon az objektumon, akkor minden alkalommal ugyanazt a számot kell visszaadnia. -
Az 1. szabály ellenkező irányban nem működik. Két különböző objektumnak lehet ugyanaz a hash kódja.
hashCode()
metódus egy int
. An int
egy 32 bites szám. Korlátozott értéktartománya van: -2 147 483 648 és +2 147 483 647 között. Más szavakkal, valamivel több mint 4 milliárd lehetséges értéke van egy int
. Most képzelje el, hogy létrehoz egy programot, amely adatokat tárol a Földön élő összes emberről. Minden személy a saját objektumának felel meg Person
(hasonlóan az Man
osztályhoz). ~7,5 milliárd ember él a bolygón. Más szóval, nem számít, milyen ügyes algoritmust írunk a konvertáláshozPerson
objektumok egy int-hez, egyszerűen nincs elég lehetséges számunk. Csak 4,5 milliárd lehetséges int értékünk van, de ennél sokkal többen vannak. Ez azt jelenti, hogy bármennyire is igyekszünk, néhány embernek ugyanaz a hash kódja lesz. Amikor ez megtörténik (két különböző objektum hash kódjai egybeesnek), ütközésnek nevezzük. A módszer felülbírálásakor hashCode()
a programozó egyik célja az ütközések lehetséges számának minimalizálása. Ha figyelembe vesszük ezeket a szabályokat, hogyan fog hashCode()
kinézni a módszer az osztályban Person
? Mint ez:
@Override
public int hashCode() {
return dnaCode;
}
Meglepődött? :) Ha megnézed a követelményeket, látni fogod, hogy mindegyiknek megfelelünk. Azok az objektumok, amelyekre a metódusunk equals()
igazat ad vissza, szintén egyenlőek lesznek a szerint hashCode()
. Ha a két objektumunk Person
egyenlő -ben equals
(vagyis azonos -ban van dnaCode
), akkor a metódusunk ugyanazt a számot adja vissza. Nézzünk egy nehezebb példát. Tegyük fel, hogy programunknak luxusautókat kell kiválasztania az autógyűjtők számára. A gyűjtés összetett hobbi lehet, számos sajátossággal. Egy adott 1963-as autó 100-szor többe kerülhet, mint egy 1964-es autó. Egy 1970-es piros autó 100-szor többe kerülhet, mint egy ugyanabban az évben gyártott kék autó. Az előző példánkban az osztállyal Person
a legtöbb mezőt (azaz az emberi jellemzőket) elvettük jelentéktelennek, és csak adnaCode
mező az összehasonlításokban. Most egy nagyon egyedi területen dolgozunk, amelyben nincsenek jelentéktelen részletek! Íme az osztályunk LuxuryAuto
:
public class LuxuryAuto {
private String model;
private int manufactureYear;
private int dollarPrice;
public LuxuryAuto(String model, int manufactureYear, int dollarPrice) {
this.model = model;
this.manufactureYear = manufactureYear;
this.dollarPrice = dollarPrice;
}
// ...getters, setters, etc.
}
Most az összehasonlításunk során minden területet figyelembe kell vennünk. Bármilyen hiba több százezer dollárba kerülhet az ügyfélnek, ezért jobb, ha túlzottan biztonságban vagyunk:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
LuxuryAuto that = (LuxuryAuto) o;
if (manufactureYear != that.manufactureYear) return false;
if (dollarPrice != that.dollarPrice) return false;
return model.equals(that.model);
}
Módszerünkben equals()
nem felejtettük el az összes ellenőrzést, amelyről korábban beszéltünk. De most összehasonlítjuk tárgyaink mindhárom mezőjét. Ehhez a programhoz abszolút egyenlőségre, azaz az egyes területek egyenlőségére van szükség. Mi a helyzet hashCode
?
@Override
public int hashCode() {
int result = model == null ? 0 : model.hashCode();
result = result + manufactureYear;
result = result + dollarPrice;
return result;
}
model
A mi osztályunkban a mező egy String. Ez kényelmes, mert az String
osztály már felülírja a hashCode()
metódust. Kiszámoljuk a model
mező hash kódját, majd hozzáadjuk a másik két numerikus mező összegét. A Java fejlesztőknek van egy egyszerű trükkje, amellyel csökkenthetik az ütközések számát: a hash kód kiszámításakor a köztes eredményt szorozzák meg egy páratlan prímszámmal. A leggyakrabban használt szám a 29 vagy a 31. A matematikai finomságokba most nem megyünk bele, de a jövőben ne feledjük, hogy a köztes eredmények kellően nagy páratlan számmal való szorzása segít a hash függvény eredményeinek "kiterítésében", ill. következésképpen csökkentse az azonos hash kóddal rendelkező objektumok számát. A LuxuryAuto módszerünknél hashCode()
ez így nézne ki:
@Override
public int hashCode() {
int result = model == null ? 0 : model.hashCode();
result = 31 * result + manufactureYear;
result = 31 * result + dollarPrice;
return result;
}
Ennek a mechanizmusnak a bonyolultságáról többet olvashat ebben a StackOverflow bejegyzésben , valamint Joshua Bloch Hatékony Java című könyvében. Végül még egy fontos szempont, amelyet érdemes megemlíteni. Minden alkalommal, amikor felülírtuk az equals()
and hashCode()
metódust, kiválasztottunk bizonyos példánymezőket, amelyeket ezek a metódusok figyelembe vesznek. Ezek a módszerek ugyanazokat a mezőket veszik figyelembe. De figyelembe vehetünk-e különböző területeket equals()
a és -ben hashCode()
? Technikailag megtehetjük. De ez egy rossz ötlet, és íme, miért:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
LuxuryAuto that = (LuxuryAuto) o;
if (manufactureYear != that.manufactureYear) return false;
return dollarPrice == that.dollarPrice;
}
@Override
public int hashCode() {
int result = model == null ? 0 : model.hashCode();
result = 31 * result + manufactureYear;
result = 31 * result + dollarPrice;
return result;
}
Itt vannak a mi equals()
és hashCode()
a módszereink az osztályhoz LuxuryAuto
. A hashCode()
metódus változatlan maradt, de a mezőt eltávolítottuk model
a equals()
metódusból. A modell már nem jellemző, amikor a equals()
módszer két objektumot hasonlít össze. De a hash kód kiszámításakor ez a mező továbbra is figyelembe vehető. Mit kapunk ennek eredményeként? Alkossunk két autót, és megtudjuk!
public class Main {
public static void main(String[] args) {
LuxuryAuto ferrariGTO = new LuxuryAuto("Ferrari 250 GTO", 1963, 70000000);
LuxuryAuto ferrariSpider = new LuxuryAuto("Ferrari 335 S Spider Scaglietti", 1963, 70000000);
System.out.println("Are these two objects equal to each other?");
System.out.println(ferrariGTO.equals(ferrariSpider));
System.out.println("What are their hash codes?");
System.out.println(ferrariGTO.hashCode());
System.out.println(ferrariSpider.hashCode());
}
}
Are these two objects equal to each other?
true
What are their hash codes?
-1372326051
1668702472
Hiba! equals()
A és módszerekhez különböző mezők használatával hashCode()
megszegtük a velük kötött szerződéseket! Két, a metódus szerint egyenlő objektumnak equals()
azonos hash kóddal kell rendelkeznie. Más értékeket kaptunk értük. Az ilyen hibák teljesen hihetetlen következményekkel járhatnak, különösen, ha hash-t használó gyűjteményekkel dolgozunk. equals()
Ennek eredményeként a és felülírásakor hashCode()
ugyanazokat a mezőket kell figyelembe vennie. Ez a lecke meglehetősen hosszú volt, de ma sokat tanultál! :) Itt az ideje, hogy visszatérjünk a feladatok megoldásához!
GO TO FULL VERSION