Likhet i egne klasser
Vi leste i kursnoater om primitive og refererte typer at sammenligning av objekter med ==
resulterte i true
bare hvis objektene var det samme objektet, mens equals
sjekket om objektene var like. Vi skal her se på hvordan vi kan få equals
til å fungere som vi ønsker for våre egne klasser.
- Standardimplementasjonen av equals
- Egendefinert equals -metode
- Krav til equals
- hashCode
- TL;DR: Autogenerert equals og hashCode
Standardimplementasjonen av equals
Hvis vi ikke selv definerer en metode med signaturen public boolean equals(Object)
i en klasse, så vil vi «arve» en standard equals-metode fra Object
-klassen. Object
en en spesiell klasse i Java, en slags «bestefar-klasse» som inneholder standard-metoder vi arver automatisk i de klassene vi lager selv. Equals-metoden der er definert slik:
public boolean equals(Object obj) {
return (this == obj);
}
Med andre ord vil et kall til equals
i praksis være det samme som å sammenligne objektene med ==
.
Dersom dette ikke er oppførselen vi ønsker, må vi selv definere en equals
-metode i klassen vår.
Egendefinert equals -metode
La oss se på et eksempel hvor sammenligning av to objekter med standard equals
-metode ikke gir ønsket resultat:
class Person {
String name;
String id;
public Person(String name, String id) {
this.name = name;
this.id = id;
}
}
public class Main {
public static void main(String[] args) {
Person p1 = new Person("Ola", "111111 12345");
Person p2 = new Person("Ola", "111111 12345");
System.out.println(p1.equals(p2)); // false
}
}
I eksempelet over ville det gitt mer mening om to personer med samme personnummer og samme navn ble regnet som like. Vi kan få dette til ved å selv definere en equals
-metode i Person
-klassen. For eksempel slik:
class Person {
private String name;
private String id;
public Person(String name, String id) {
this.name = name;
this.id = id;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (obj == this) {
return true;
}
if (!(obj instanceof Person)) {
return false;
}
Person other = ((Person) obj);
return this.name.equals(other.name) && this.id.equals(other.id);
}
// TODO: Husk å implementere hashCode() også! Se avsnittet senere!
}
public class Main {
public static void main(String[] args) {
Person p1 = new Person("Ola", "111111 12345");
Person p2 = new Person("Ola", "111111 12345");
System.out.println(p1.equals(p2)); // true
}
}
Punkter å merke seg:
@Override
: det er svært god stil å skrive @Override før metode-signaturen til metoder vi arver. Programmet vil fremdeles virke uten at vi skriver det, men da er det en hel del feilsøking og refaktorering kodeeditoren vår ikke klarer å hjelpe oss med, og det er mye vanskeligere å sette seg inn i koden for andre. Å bruke @Override er såpass viktig at du må regne med poengtrekk på eksamen eller andre innleveringer hvis du glemmer det.- Parameteren i
equals
-metoden skal ha typenObject
. Alle objekter har denne typen, uansett hvilken klasse objektet tilhører. - Det er opp til oss selv å sjekke om objektet vi får inn i metoden er av den typen vi forventer. Dette gjør vi med
instanceof
-operatoren. - Vi må caste objektet vi får som argument til den typen vi forventer etter at vi har sjekket at objekter faktisk har riktig type. Dette gjør vi i eksempelet med
((Person) obj)
.
Krav til equals
Java tillater oss å implementere equals hvordan vi selv ønsker, men for å skrive god kode er det viktig at vi følger kontrakten som er definert for equals-metoden. Kontrakten finner du ved å finlese javadoc-kommentarene til equals
-metoden i Object
-klassen, og er sammenfattet her.
Anta at x
ikke er null
. Da må equals
-metoden oppfylle følgende krav:
- Refleksivitet:
x.equals(x)
skal returneretrue
uansett hvax
er. - Symmetri:
x.equals(y)
skal returneretrue
hvis og bare hvisy.equals(x)
gjør det. - Transitivitet: Hvis
x.equals(y)
returnerertrue
ogy.equals(z)
returnerertrue
, så skalx.equals(z)
returneretrue
. - Konsistens:
x.equals(y)
skal returnere samme verdi (ententrue
ellerfalse
) hver gang så lenge objektene ikke endrer seg. - Null-sikkerhet:
x.equals(null)
skal returnerefalse
uansett hvax
er.
Dersom kontrakten overholdes, vil equals
-metoden fungere som forventet, og den vil samsvare med hva vi kaller en ekvivalens-relasjon i matematikk. Dersom vi ikke følger kontrakten, kan det føre til all slags trøbbel.
I tillegg til det som er nevnt over, er det et siste krav som må innfris:
- dersom
x.equals(y)
returnerertrue
, så må ogsåx.hashCode() == y.hashCode()
evaluere tiltrue
.
Dette siste punktet er i eksempelet i forrige avsnitt over ikke innfridd.
hashCode -metoden
hashCode-metoden er på samme måte som equals en metode som hører til Object
-klassen. Alle klasser arver derfor en standard hashCode-metode med signaturen public int hashCode()
hvis man ikke implementerer den selv. Metoden returnerer en enkelt int
som på sin egen måte beskriver objektet. I den standard hashCode-metoden som arves fra Object returneres minneadressen til objektet (PS: dette er egentlig ikke sant, men hvis vi forestiller oss en eventyrverden der objekter aldri endrer minneadresse kunne det vært sant).
Tallet brukes under panseret i datastrukturer som oppslagsverk (map/dictionary) og mengder (set). I INF101 er det egentlig ikke interessant for oss å vite hva dette tallet er, eller hvordan det blir regnet ut; poenget er bare at to objekter som er like som definert av equals
-metoden, må returnere samme verdi fra hashCode
-metoden. Ellers vil ikke oppslagsverk, mengder eller andre datastrukturer basert på hashing fungere.
Av effektivitetshensyn er det ønskelig at to ulike objekter alltid returnerer ulike verdier fra
hashCode
-metoden; men dette er ikke et krav med tanke på korrekthet, og dessuten teoretisk umulig å oppnå i mange tilfeller. Det vil kun gå ut over kjøretiden dersom vi ikke klarer å oppnå det. Datastrukturer basert på hashing vil være raskere jo større sannsynligheten er for at ulike objekter returnerer ulike hash-verdier.
Først et eksempel på hva som går galt hvis vi ikke følger kontrakten, og bruker Person
-klassen fra forrige avsnitt som ikke har implementert hashCode
-metoden:
// et sted i koden...
HashSet<Person> persons = new HashSet<>();
persons.add(new Person("Ola", "111111 12345"));
persons.add(new Person("Kari", "222222 12345"));
// mye senere...
Person p = new Person("Ola", "111111 12345");
System.out.println(persons.contains(p)); // false!
import java.util.HashSet;
class Person {
String name;
String id;
public Person(String name, String id) {
this.name = name;
this.id = id;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (obj == this) {
return true;
}
if (!(obj instanceof Person)) {
return false;
}
Person other = ((Person) obj);
return this.name.equals(other.name) && this.id.equals(other.id);
}
// Mangler hashCode()!
}
public class Main {
public static void main(String[] args) {
// et sted i koden...
HashSet<Person> persons = new HashSet<>();
persons.add(new Person("Ola", "111111 12345"));
persons.add(new Person("Kari", "222222 12345"));
// mye senere...
Person p = new Person("Ola", "111111 12345");
System.out.println(persons.contains(p)); // false!
}
}
Svaret over blir feil. Det er fordi vi ikke har implementert hashCode
-metoden. Heldigvis er den lett å implementere:
@Override
public int hashCode() {
return Objects.hash(name, id);
}
import java.util.HashSet;
import java.util.Objects;
class Person {
String name;
String id;
public Person(String name, String id) {
this.name = name;
this.id = id;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (obj == this) {
return true;
}
if (!(obj instanceof Person)) {
return false;
}
Person other = ((Person) obj);
return this.name.equals(other.name) && this.id.equals(other.id);
}
@Override
public int hashCode() {
return Objects.hash(name, id);
}
}
public class Main {
public static void main(String[] args) {
// et sted i koden...
HashSet<Person> persons = new HashSet<>();
persons.add(new Person("Ola", "111111 12345"));
persons.add(new Person("Kari", "222222 12345"));
// mye senere...
Person p = new Person("Ola", "111111 12345");
System.out.println(persons.contains(p)); // true
}
}
Autogenerert equals og hashCode
De fleste kodeeditorer kan generere hashCode
og equals
-metoder automatisk for deg. Da kan du enkelt og greit markere hvilke feltvariabler du ønsker skal inngå i sammenligningsgrunnlaget. Hvis din editor gir deg valget, må du velge de samme variablene for å generere både hashCode og equals, ellers blir det fort feil.
- IntelliJ: offisiell dokumentasjon
- VS Code: offisiell dokumentasjon
- Eclipse: instruksjoner fra baeldung.com
Husk å generere på nytt hvis du legger til eller fjerner feltvariabler.