equals()
en hashCode()
. Dit is niet de eerste keer dat we ze ontmoeten: de CodeGym-cursus begint met een korte les over equals()
— lees het als je het bent vergeten of nog niet eerder hebt gezien... 
==
operator, omdat ==
vergelijkingen referenties zijn. Hier is ons voorbeeld met auto's uit een recente les:
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);
}
}
Console-uitvoer:
false
Het lijkt erop dat we twee identieke objecten hebben gemaakt Car
: de waarden van de overeenkomstige velden van de twee auto-objecten zijn hetzelfde, maar het resultaat van de vergelijking is nog steeds onjuist. We weten de reden al: de verwijzingen car1
en car2
verwijzen naar verschillende geheugenadressen, dus ze zijn niet gelijk. Maar we willen nog steeds de twee objecten vergelijken, niet twee referenties. De beste oplossing voor het vergelijken van objecten is de equals()
methode.
is gelijk aan () methode
U herinnert zich misschien dat we deze methode niet helemaal opnieuw maken, maar dat we deze overschrijven: deequals()
methode wordt gedefinieerd in de Object
klasse. Dat gezegd hebbende, in zijn gebruikelijke vorm heeft het weinig zin:
public boolean equals(Object obj) {
return (this == obj);
}
Dit is hoe de equals()
methode in de klasse wordt gedefinieerd Object
. Dit is weer een vergelijking van referenties. Waarom hebben ze het zo gemaakt? Welnu, hoe weten de makers van de taal welke objecten in uw programma als gelijk worden beschouwd en welke niet? :) Dit is het belangrijkste punt van de equals()
methode — de maker van een klasse is degene die bepaalt welke kenmerken worden gebruikt bij het controleren van de gelijkheid van objecten van de klasse. Dan overschrijf je de equals()
methode in je klas. Als u de betekenis van "bepaalt welke kenmerken" niet helemaal begrijpt, laten we eens kijken naar een voorbeeld. Hier is een eenvoudige klasse die een man voorstelt: 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.
}
Stel dat we een programma schrijven dat moet bepalen of twee mensen een eeneiige tweeling zijn of gewoon lookalikes. We hebben vijf kenmerken: neusgrootte, oogkleur, haarstijl, de aanwezigheid van littekens en DNA-testresultaten (voor de eenvoud stellen we dit voor als een geheel getal). Welke van deze kenmerken zou volgens u ons programma in staat stellen om identieke tweelingen te identificeren? 
equals()
werkwijze? We moeten het overschrijven in deMan
klasse, rekening houdend met de vereisten van ons programma. De methode moet het int dnaCode
veld van de twee objecten vergelijken. Als ze gelijk zijn, dan zijn de objecten gelijk.
@Override
public boolean equals(Object o) {
Man man = (Man) o;
return dnaCode == man.dnaCode;
}
Is het echt zo simpel? Niet echt. We hebben iets over het hoofd gezien. Voor onze objecten hebben we slechts één veld geïdentificeerd dat relevant is voor het vaststellen van objectgelijkheid: dnaCode
. Stel je nu voor dat we niet 1, maar 50 relevante velden hebben. En als alle 50 velden van twee objecten gelijk zijn, dan zijn de objecten gelijk. Zo'n scenario is ook mogelijk. Het grootste probleem is dat het vaststellen van gelijkheid door het vergelijken van 50 velden een tijdrovend en arbeidsintensief proces is. Man
Stel je nu voor dat we naast onze klasse een Woman
klasse hebben met exact dezelfde velden die bestaan in Man
. Als een andere programmeur onze klassen gebruikt, kan hij of zij gemakkelijk de volgende code schrijven:
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));
}
In dit geval heeft het geen zin om de veldwaarden te controleren: we kunnen gemakkelijk zien dat we objecten van twee verschillende klassen hebben, dus ze kunnen onmogelijk gelijk zijn! Dit betekent dat we een controle aan de equals()
methode moeten toevoegen, waarbij de klassen van de vergeleken objecten worden vergeleken. Goed dat we daaraan hebben gedacht!
@Override
public boolean equals(Object o) {
if (getClass() != o.getClass()) return false;
Man man = (Man) o;
return dnaCode == man.dnaCode;
}
Maar misschien zijn we nog iets vergeten? Hmm... We moeten op zijn minst controleren of we een object niet met zichzelf vergelijken! Als referenties A en B naar hetzelfde geheugenadres wijzen, dan zijn ze hetzelfde object en hoeven we geen tijd te verspillen aan het vergelijken van 50 velden.
@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;
}
Het kan ook geen kwaad om een controle toe te voegen voor null
: geen object kan gelijk zijn aan null
. Dus als de methodeparameter null is, heeft het geen zin om extra controles uit te voeren. Met dit alles in gedachten, ziet onze equals()
methode voor de Man
klas er als volgt uit:
@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;
}
Wij voeren alle bovengenoemde eerste controles uit. Aan het eind van de dag, als:
- we vergelijken twee objecten van dezelfde klasse
- en de vergeleken objecten zijn niet hetzelfde object
- en het gepasseerde object niet
null
dnaCode
velden van de twee objecten. equals()
Houd u bij het overschrijven van de methode aan de volgende vereisten:
-
Reflexiviteit.
Wanneer de
equals()
methode wordt gebruikt om een object met zichzelf te vergelijken, moet deze true retourneren.
Aan deze eis hebben wij inmiddels voldaan. Onze werkwijze omvat:if (this == o) return true;
-
Symmetrie.
Als
a.equals(b) == true
, danb.equals(a)
moet terugkerentrue
.
Ook onze werkwijze voldoet aan deze eis. -
Transitiviteit.
Als twee objecten gelijk zijn aan een derde object, dan moeten ze aan elkaar gelijk zijn.
Alsa.equals(b) == true
ena.equals(c) == true
, danb.equals(c)
moet ook waar worden geretourneerd. -
Vasthoudendheid.
Het resultaat van
equals()
moet alleen veranderen als de betrokken velden worden gewijzigd. Als de gegevens van de twee objecten niet veranderen,equals()
moet het resultaat van altijd hetzelfde zijn. -
Ongelijkheid met
null
.Voor elk object
a.equals(null)
moet false worden geretourneerd.
Dit is niet alleen een reeks "nuttige aanbevelingen", maar eerder een strikt contract , uiteengezet in de Oracle-documentatie
hashCode()-methode
Laten we het nu hebben over dehashCode()
methode. Waarom is het nodig? Met precies hetzelfde doel: objecten vergelijken. Maar dat hebben we al equals()
! Waarom een andere methode? Het antwoord is simpel: om de prestaties te verbeteren. Een hash-functie, weergegeven in Java met behulp van de hashCode()
methode, retourneert een numerieke waarde met een vaste lengte voor elk object. In Java hashCode()
retourneert de methode een 32-bits getal ( int
) voor elk object. Het vergelijken van twee getallen gaat veel sneller dan het vergelijken van twee objecten met behulp van de equals()
methode, vooral als die methode rekening houdt met veel velden. Als ons programma objecten vergelijkt, is dat veel eenvoudiger met een hashcode. Alleen als de objecten op basis van de hashCode()
methode gelijk zijn, gaat de vergelijking door naar deequals()
methode. Dit is trouwens hoe hash-gebaseerde datastructuren werken, bijvoorbeeld de bekende HashMap
! De hashCode()
methode equals()
wordt, net als de methode, overschreven door de ontwikkelaar. En net als equals()
, heeft de hashCode()
methode officiële vereisten die worden beschreven in de Oracle-documentatie:
-
Als twee objecten gelijk zijn (dwz de
equals()
methode retourneert waar), dan moeten ze dezelfde hashcode hebben.Anders zouden onze methoden zinloos zijn. Zoals we hierboven vermeldden,
hashCode()
moet er eerst een controle plaatsvinden om de prestaties te verbeteren. Als de hash-codes verschillend waren, zou de controle false retourneren, ook al zijn de objecten eigenlijk gelijk volgens hoe we deequals()
methode hebben gedefinieerd. -
Als de
hashCode()
methode meerdere keren op hetzelfde object wordt aangeroepen, moet deze elke keer hetzelfde getal teruggeven. -
Regel 1 werkt niet averechts. Twee verschillende objecten kunnen dezelfde hashcode hebben.
hashCode()
methode retourneert een int
. An int
is een 32-bits getal. Het heeft een beperkt bereik van waarden: van -2.147.483.648 tot +2.147.483.647. Met andere woorden, er zijn iets meer dan 4 miljard mogelijke waarden voor een int
. Stel je nu voor dat je een programma maakt om gegevens op te slaan over alle mensen die op aarde leven. Elke persoon komt overeen met zijn eigen Person
object (vergelijkbaar met de Man
klasse). Er leven zo'n 7,5 miljard mensen op de planeet. Met andere woorden, hoe slim het algoritme ook is dat we schrijven voor conversiePerson
bezwaar maakt tegen een int, hebben we simpelweg niet genoeg mogelijke getallen. We hebben slechts 4,5 miljard mogelijke int-waarden, maar er zijn veel meer mensen dan dat. Dit betekent dat, hoe hard we ook proberen, sommige mensen dezelfde hashcodes zullen hebben. Wanneer dit gebeurt (hashcodes vallen samen voor twee verschillende objecten) noemen we dit een botsing. Bij het negeren van de hashCode()
methode is een van de doelstellingen van de programmeur het minimaliseren van het potentiële aantal botsingen. Rekening houdend met al deze regels, hoe zal de methode er in de klas hashCode()
uitzien ? Person
Soortgelijk:
@Override
public int hashCode() {
return dnaCode;
}
Verrast? :) Als je naar de eisen kijkt, zul je zien dat we ze allemaal naleven. Objecten waarvoor onze equals()
methode true retourneert, zullen ook gelijk zijn volgens hashCode()
. Als onze twee Person
objecten gelijk zijn in equals
(dat wil zeggen, ze hebben dezelfde dnaCode
), dan retourneert onze methode hetzelfde getal. Laten we een moeilijker voorbeeld bekijken. Stel dat ons programma luxe auto's voor autoverzamelaars zou moeten selecteren. Verzamelen kan een complexe hobby zijn met veel eigenaardigheden. Een bepaalde auto uit 1963 kan 100 keer meer kosten dan een auto uit 1964. Een rode auto uit 1970 kan 100 keer meer kosten dan een blauwe auto van hetzelfde merk uit hetzelfde jaar. 
Person
klasse, hebben we de meeste velden (d.w.z. menselijke kenmerken) weggegooid als onbeduidend en hebben we alleen dednaCode
veld in vergelijkingen. We werken nu in een heel eigenzinnig rijk, waarin geen onbelangrijke details zijn! Hier is onze LuxuryAuto
klas:
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.
}
Nu moeten we alle velden in onze vergelijkingen beschouwen. Elke fout kan een klant honderdduizenden dollars kosten, dus het is beter om overdreven veilig te zijn:
@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);
}
In onze equals()
methode zijn we alle controles waar we het eerder over hadden niet vergeten. Maar nu vergelijken we elk van de drie velden van onze objecten. Voor dit programma hebben we absolute gelijkheid nodig, dwz gelijkheid van elk veld. Hoe zit het met hashCode
?
@Override
public int hashCode() {
int result = model == null ? 0 : model.hashCode();
result = result + manufactureYear;
result = result + dollarPrice;
return result;
}
Het model
veld in onze klasse is een string. Dit is handig, omdat de String
klasse de methode al overschrijft hashCode()
. We berekenen de model
hashcode van het veld en tellen vervolgens de som van de andere twee numerieke velden op. Java-ontwikkelaars hebben een simpele truc die ze gebruiken om het aantal botsingen te verminderen: vermenigvuldig bij het berekenen van een hashcode het tussenresultaat met een oneven priemgetal. Het meest gebruikte getal is 29 of 31. We zullen nu niet ingaan op de wiskundige subtiliteiten, maar onthoud in de toekomst dat het vermenigvuldigen van tussenresultaten met een voldoende groot oneven getal helpt om de resultaten van de hashfunctie te "spreiden" en, verminder daarom het aantal objecten met dezelfde hashcode. Voor onze hashCode()
methode in LuxuryAuto zou het er zo uitzien:
@Override
public int hashCode() {
int result = model == null ? 0 : model.hashCode();
result = 31 * result + manufactureYear;
result = 31 * result + dollarPrice;
return result;
}
Je kunt meer lezen over alle fijne kneepjes van dit mechanisme in dit bericht op StackOverflow , evenals in het boek Effective Java van Joshua Bloch. Tot slot nog een belangrijk punt dat het vermelden waard is. equals()
Elke keer dat we de methode and overschreven hashCode()
, selecteerden we bepaalde instantievelden waarmee in deze methoden rekening wordt gehouden. Deze methoden houden rekening met dezelfde velden. Maar kunnen we verschillende gebieden in equals()
en beschouwen hashCode()
? Technisch gezien kunnen we dat. Maar dit is een slecht idee, en hier is waarom:
@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;
}
Hier zijn onze equals()
en hashCode()
methoden voor de LuxuryAuto
klas. De hashCode()
methode bleef ongewijzigd, maar we hebben het model
veld uit de equals()
methode verwijderd. Het model is niet langer een kenmerk dat wordt gebruikt wanneer de equals()
methode twee objecten vergelijkt. Maar bij het berekenen van de hashcode wordt toch rekening gehouden met dat veld. Wat krijgen we als resultaat? Laten we twee auto's maken en erachter komen!
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
Fout! Door verschillende velden te gebruiken voor de methoden equals()
en hashCode()
hebben we de contracten geschonden die ervoor zijn opgesteld! Twee objecten die volgens de equals()
methode gelijk zijn, moeten dezelfde hashcode hebben. We hebben er verschillende waarden voor gekregen. Dergelijke fouten kunnen tot absoluut ongelooflijke gevolgen leiden, vooral bij het werken met collecties die een hash gebruiken. Daarom moet u, wanneer u equals()
en overschrijft hashCode()
, rekening houden met dezelfde velden. Deze les was vrij lang, maar je hebt veel geleerd vandaag! :) Nu is het tijd om terug te gaan naar het oplossen van taken!
GO TO FULL VERSION