Java is gelijk aan () en hashCode () Contracten

1. Overzicht

In deze tutorial introduceren we twee methoden die nauw bij elkaar passen: is gelijk aan () en hashCode (). We zullen ons concentreren op hun relatie met elkaar, hoe ze correct kunnen worden opgeheven en waarom we beide of geen van beide moeten negeren.

2. is gelijk aan ()

De Voorwerp klasse definieert zowel de is gelijk aan () en hashCode () methoden - wat betekent dat deze twee methoden impliciet zijn gedefinieerd in elke Java-klasse, inclusief degene die we maken:

class Money {int bedrag; Tekenreeks currencyCode; }
Geldinkomen = nieuw geld (55, "USD"); Gelduitgaven = nieuw geld (55, "USD"); boolean balanced = income.equals (onkosten)

We zouden verwachten inkomen.gelijken (onkosten) terugbrengen waar. Maar met de Geld klasse in zijn huidige vorm, zal het niet.

De standaardimplementatie van is gelijk aan () in de klas Voorwerp zegt dat gelijkheid hetzelfde is als objectidentiteit. En inkomen en onkosten zijn twee verschillende gevallen.

2.1. Overheersend is gelijk aan ()

Laten we de is gelijk aan () methode zodat het niet alleen de objectidentiteit in overweging neemt, maar ook de waarde van de twee relevante eigenschappen:

@Override public boolean equals (Object o) if (o == this) return true; if (! (o instanceof Money)) return false; Geld overig = (Geld) o; boolean currencyCodeEquals = (this.currencyCode == null && other.currencyCode == null) 

2.2. is gelijk aan () Contract

Java SE definieert een contract dat onze implementatie van het is gelijk aan () methode moet voldoen. De meeste criteria zijn gezond verstand. De is gelijk aan () methode moet zijn:

  • reflexief: een object moet zichzelf evenaren
  • symmetrisch: x.equals (y) moet hetzelfde resultaat retourneren als y. is gelijk aan (x)
  • transitief: als x.equals (y) en y.equals (z) dan ook x.equals (z)
  • consequent: de waarde van is gelijk aan () zou alleen moeten veranderen als een eigenschap die is opgenomen in is gelijk aan () veranderingen (geen willekeur toegestaan)

We kunnen de exacte criteria opzoeken in de Java SE Docs voor de Voorwerp klasse.

2.3. Overtreding is gelijk aan () Symmetrie met overerving

Als de criteria voor is gelijk aan () is zo gezond verstand, hoe kunnen we het überhaupt overtreden? Goed, schendingen komen het vaakst voor als we een klasse uitbreiden die overschreven is is gelijk aan (). Laten we eens kijken naar een Waardebon klasse die onze Geld klasse:

klasse WrongVoucher breidt Money {private String store; @Override openbare boolean is gelijk aan (Object o) // andere methoden}

Op het eerste gezicht is de Waardebon class en zijn overschrijving voor is gelijk aan () lijken correct te zijn. En beide is gelijk aan () methoden gedragen zich correct zolang we vergelijken Geld naar Geld of Waardebon naar Waardebon. Maar wat gebeurt er als we deze twee objecten vergelijken?

Geld contant = nieuw geld (42, "USD"); WrongVoucher-voucher = nieuwe WrongVoucher (42, "USD", "Amazon"); voucher.equals (cash) => false // Zoals verwacht. cash.equals (voucher) => true // Dat is fout.

Dat is in strijd met de symmetriecriteria van de is gelijk aan () contract.

2.4. Oplossen is gelijk aan () Symmetrie met compositie

Om deze valkuil te vermijden, moeten we geef de voorkeur aan compositie boven overerving.

In plaats van subclassificatie Geld, laten we een Waardebon klas met een Geld eigendom:

klasse Voucher {privé Geldwaarde; particuliere String-winkel; Voucher (int bedrag, String currencyCode, String store) {this.value = new Money (amount, currencyCode); this.store = winkel; } @Override openbare booleaanse waarde is gelijk aan (Object o) // andere methoden}

En nu, is gelijk aan zal symmetrisch werken zoals het contract vereist.

3. hashCode ()

hashCode () geeft een geheel getal terug dat het huidige exemplaar van de klasse vertegenwoordigt. We moeten deze waarde berekenen in overeenstemming met de definitie van gelijkheid voor de klasse. Dus als we de is gelijk aan () methode, we moeten ook overschrijven hashCode ().

Raadpleeg onze gids voor meer informatie hashCode ().

3.1. hashCode () Contract

Java SE definieert ook een contract voor het hashCode () methode. Een grondige blik erop laat zien hoe nauw verwant is hashCode () en is gelijk aan () zijn.

Alle drie de criteria in het contract van hashCode () noem in sommige opzichten de is gelijk aan () methode:

  • interne consistentie: de waarde van hashCode () mag alleen veranderen als een woning in is gelijk aan () veranderingen
  • staat gelijk aan consistentie: objecten die aan elkaar gelijk zijn, moeten dezelfde hashCode retourneren
  • botsingen: ongelijke objecten kunnen dezelfde hashCode hebben

3.2. De consistentie van hashCode () en is gelijk aan ()

Het 2e criterium van het hashCode-methodencontract heeft een belangrijk gevolg: Als we equals () overschrijven, moeten we ook hashCode () overschrijven. En dit is verreweg de meest voorkomende schending met betrekking tot de contracten van de is gelijk aan () en hashCode () methoden.

Laten we een dergelijk voorbeeld bekijken:

klasse Team {String city; String afdeling; @Override public final boolean equals (Object o) {// implementatie}}

De Team alleen klasse overschrijft is gelijk aan (), maar het gebruikt nog steeds impliciet de standaardimplementatie van hashCode () zoals gedefinieerd in de Voorwerp klasse. En dit levert een ander resultaat op hashCode () voor elk exemplaar van de klas. Dit is in strijd met de tweede regel.

Als we er nu twee maken Team objecten, beide met stad "New York" en afdeling "marketing", ze zullen gelijk zijn, maar ze zullen verschillende hashCodes retourneren.

3.3. Hash kaart Sleutel met een inconsistent hashCode ()

Maar waarom is de contractbreuk in onze Team klasse een probleem? Welnu, het probleem begint wanneer het om sommige hash-gebaseerde collecties gaat. Laten we proberen onze Team klasse als een sleutel van een Hash kaart:

Kaartleiders = nieuwe HashMap (); leaders.put (nieuw team ("New York", "ontwikkeling"), "Anne"); leiders.put (nieuw team ("Boston", "ontwikkeling"), "Brian"); leaders.put (nieuw team ("Boston", "marketing"), "Charlie"); Team myTeam = nieuw team ("New York", "ontwikkeling"); String myTeamLeader = leaders.get (myTeam);

We zouden verwachten myTeamLeader om "Anne" terug te geven. Maar met de huidige code is dat niet het geval.

Als we instanties van de Team klasse als Hash kaart toetsen, moeten we de hashCode () methode zodat het voldoet aan het contract: Gelijke objecten retourneren hetzelfde hashCode.

Laten we een voorbeeldimplementatie bekijken:

@Override openbare finale int hashCode () {int resultaat = 17; if (city! = null) {resultaat = 31 * resultaat + city.hashCode (); } if (afdeling! = null) {resultaat = 31 * resultaat + afdeling.hashCode (); } resultaat teruggeven; }

Na deze wijziging leaders.get (myTeam) geeft "Anne" terug zoals verwacht.

4. Wanneer overschrijven we is gelijk aan () en hashCode ()?

Over het algemeen willen we beide of geen van beide opheffen. We hebben zojuist in paragraaf 3 de ongewenste gevolgen gezien als we deze regel negeren.

Domain-Driven Design kan ons helpen beslissen over de omstandigheden waarin we ze zouden moeten laten. Voor entiteitsklassen - voor objecten met een intrinsieke identiteit - is de standaardimplementatie vaak logisch.

Echter, voor waardeobjecten geven we meestal de voorkeur aan gelijkheid op basis van hun eigenschappen. Dus willen opheffen is gelijk aan () en hashCode (). Onthoud onze Geld class uit Sectie 2:55 USD is gelijk aan 55 USD - zelfs als het twee afzonderlijke instanties zijn.

5. Implementatiehelpers

We schrijven de implementatie van deze methoden meestal niet met de hand. Zoals te zien is, zijn er nogal wat valkuilen.

Een veelgebruikte manier is om onze IDE het is gelijk aan () en hashCode () methoden.

Apache Commons Lang en Google Guava hebben helperklassen om het schrijven van beide methoden te vereenvoudigen.

Project Lombok biedt ook een @EqualsAndHashCode annotatie. Merk nogmaals op hoe is gelijk aan () en hashCode () "Gaan samen" en hebben zelfs een gemeenschappelijke annotatie.

6. Verificatie van de contracten

Als we willen controleren of onze implementaties voldoen aan de Java SE-contracten en ook aan enkele best practices, we kunnen de EqualsVerifier-bibliotheek gebruiken.

Laten we de EqualsVerifier Maven-testafhankelijkheid toevoegen:

 nl.jqno.equalsverifier equalsverifier 3.0.3 test 

Laten we verifiëren dat onze Team klasse volgt de is gelijk aan () en hashCode () contracten:

@Test openbare leegte equalsHashCodeContracts () {EqualsVerifier.forClass (Team.class) .verify (); }

Het is vermeldenswaard dat EqualsVerifier test zowel de is gelijk aan () en hashCode () methoden.

EqualsVerifier is veel strenger dan het Java SE-contract. Het zorgt er bijvoorbeeld voor dat onze methoden geen NullPointerException. Het dwingt ook af dat beide methoden, of de klasse zelf, definitief zijn.

Het is belangrijk om dat te beseffen de standaardconfiguratie van EqualsVerifier staat alleen onveranderlijke velden toe. Dit is een strengere controle dan wat het Java SE-contract toestaat. Dit sluit aan bij een aanbeveling van Domain-Driven Design om waardeobjecten onveranderlijk te maken.

Als we sommige ingebouwde beperkingen niet nodig vinden, kunnen we een onderdrukken (Warning.SPECIFIC_WARNING) naar onze EqualsVerifier bellen.

7. Conclusie

In dit artikel hebben we de is gelijk aan () en hashCode () contracten. We moeten onthouden om:

  • Overschrijf altijd hashCode () als we negeren is gelijk aan ()
  • Overschrijven is gelijk aan () en hashCode () voor waardeobjecten
  • Let op de valkuilen van uitbreidende klassen die zijn overschreven is gelijk aan () en hashCode ()
  • Overweeg om een ​​IDE of een bibliotheek van derden te gebruiken voor het genereren van het is gelijk aan () en hashCode () methoden
  • Overweeg om EqualsVerifier te gebruiken om onze implementatie te testen

Ten slotte zijn alle codevoorbeelden te vinden op GitHub.


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