equals și hashCode în Java

Clase și obiecte Java 1.0 (Objects.equals / Objects.hash: Java 7)

equals decide când două obiecte sunt „egale” ca valoare; hashCode trebuie rescris împreună cu el, altfel HashMap și HashSet nu funcționează corect.

Sintaxă
@Override
public boolean equals(Object o) {
    return o instanceof Elev e && cnp.equals(e.cnp);
}

@Override
public int hashCode() {
    return cnp.hashCode();   // sau Objects.hash(camp1, camp2)
}

Ce face equals și hashCode

== pe obiecte verifică dacă e vorba de același obiect. equals verifică dacă două obiecte au aceeași valoare. Versiunea moștenită din Object face tot ==, așa că pentru clasele tale trebuie rescrisă, comparând câmpurile relevante. String, Integer, LocalDate și colecțiile au deja equals corect.

Regula de aur: dacă două obiecte sunt egale după equals, trebuie să aibă același hashCode. HashMap și HashSet caută întâi după hash, apoi verifică cu equals. Cu equals rescris, dar fără hashCode, două obiecte „egale” ajung în locuri diferite și apar duplicate. Compilat cu -Xlint, javac avertizează ([overrides]).

Ajutoare: Objects.equals(a, b) compară sigur și când valorile pot fi null, iar Objects.hash(a, b, c) combină hash-urile câmpurilor. Un record le generează automat pe amândouă din toate componentele.

Exemple

Exemplul 1: Fără equals: duplicate în HashSet

Clasa nu rescrie nici equals, nici hashCode, deci două obiecte cu același CNP sunt considerate diferite: setul are 2 elemente, iar contains nu găsește persoana.

import java.util.HashSet;
import java.util.Set;

class Persoana {
    final String cnp;
    final String nume;

    Persoana(String cnp, String nume) {
        this.cnp = cnp;
        this.nume = nume;
    }
}

public class Main {
    public static void main(String[] args) {
        Persoana a = new Persoana("2990101223344", "Ana");
        Persoana b = new Persoana("2990101223344", "Ana");
        System.out.println("a.equals(b): " + a.equals(b));

        Set<Persoana> set = new HashSet<>();
        set.add(a);
        set.add(b);
        System.out.println("elemente în set: " + set.size());
        System.out.println("conține o Ana nouă? " + set.contains(new Persoana("2990101223344", "Ana")));
    }
}

Rezultat

a.equals(b): false
elemente în set: 2
conține o Ana nouă? false

Exemplul 2: equals și hashCode scrise corect

Două persoane sunt egale dacă au același CNP. hashCode folosește același câmp, deci setul păstrează un singur element, iar căutarea în HashMap funcționează.

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

class Persoana {
    final String cnp;
    final String nume;

    Persoana(String cnp, String nume) {
        this.cnp = cnp;
        this.nume = nume;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Persoana p)) return false;
        return Objects.equals(cnp, p.cnp);
    }

    @Override
    public int hashCode() {
        return Objects.hash(cnp);
    }
}

public class Main {
    public static void main(String[] args) {
        Persoana a = new Persoana("2990101223344", "Ana");
        Persoana b = new Persoana("2990101223344", "Ana Maria");
        System.out.println("a.equals(b): " + a.equals(b));
        System.out.println("același hashCode: " + (a.hashCode() == b.hashCode()));

        Set<Persoana> set = new HashSet<>(Set.of(a));
        set.add(b);
        System.out.println("elemente în set: " + set.size());

        Map<Persoana, Double> solduri = new HashMap<>();
        solduri.put(a, 350.0);
        System.out.println("sold găsit cu alt obiect: " + solduri.get(new Persoana("2990101223344", "?")));
        System.out.println("equals(null): " + a.equals(null));
    }
}

Rezultat

a.equals(b): true
același hashCode: true
elemente în set: 1
sold găsit cu alt obiect: 350.0
equals(null): false

Exemplul 3: Records: equals și hashCode automate

Record-ul compară toate componentele. Două coordonate cu aceleași valori sunt egale și ajung o singură dată în set.

import java.util.LinkedHashSet;
import java.util.Set;

record Coordonata(double lat, double lon) { }

public class Main {
    public static void main(String[] args) {
        Set<Coordonata> vizitate = new LinkedHashSet<>();
        vizitate.add(new Coordonata(47.16, 27.58));   // Iași
        vizitate.add(new Coordonata(46.77, 23.59));   // Cluj
        vizitate.add(new Coordonata(47.16, 27.58));   // Iași din nou
        System.out.println(vizitate);
        System.out.println("egale: " + new Coordonata(1, 2).equals(new Coordonata(1, 2)));
    }
}

Rezultat

[Coordonata[lat=47.16, lon=27.58], Coordonata[lat=46.77, lon=23.59]]
egale: true

Greșeli frecvente

  • Rescrii equals fără hashCode: obiectele egale se dublează în HashSet și nu se găsesc în HashMap; cu -Xlint primești [overrides] Class Persoana overrides equals, but neither it nor any superclass overrides hashCode method.
  • Scrii public boolean equals(Persoana p) în loc de equals(Object o). Aceasta este o supraîncărcare, nu o rescriere, iar colecțiile o ignoră. @Override ar fi prins greșeala.
  • Folosești în hashCode câmpuri care se schimbă după ce obiectul a fost pus într-un HashSet: obiectul nu mai este găsit. Folosește câmpuri final.
  • Compari șiruri cu ==: "Iași" == text poate da false chiar dacă textul este același. Folosește equals.

Comenzi înrudite

  • record: record declară o clasă imutabilă pentru date: constructorul, accesorii, equals, hashCode și toString sunt generați automat.
  • Clase și obiecte (class): O clasă (cuvântul cheie class) descrie un tip nou: câmpuri pentru date și metode pentru comportament; obiectele sunt instanțele ei.
  • instanceof: instanceof verifică dacă un obiect este de un anumit tip; în forma nouă declară și o variabilă deja convertită la acel tip.
  • Comparatori (Comparable și Comparator): Comparable dă ordinea naturală a unei clase (compareTo); Comparator descrie orice altă ordine și se combină cu comparing, thenComparing și reversed.