equals() e hashCode() in Java

I metodi equals() e hashCode() di Java sono presenti nella classe Object. Quindi ogni classe Java ottiene l’implementazione predefinita di equals() e hashCode(). In questo post analizzeremo nel dettaglio i metodi equals() e hashCode() di Java.

Java equals()

La classe Object definisce il metodo equals() come segue:

public boolean equals(Object obj) {
        return (this == obj);
}

Secondo la documentazione di Java per il metodo equals(), ogni implementazione dovrebbe seguire i seguenti principi:

  • Per qualsiasi oggetto x, x.equals(x) dovrebbe restituire true.
  • Per qualsiasi due oggetti x e y, x.equals(y) dovrebbe restituire true solo se y.equals(x) restituisce true.
  • Per più oggetti x, y e z, se x.equals(y) restituisce true e y.equals(z) restituisce true, allora x.equals(z) dovrebbe restituire true.
  • Le chiamate multiple a x.equals(y) dovrebbero restituire lo stesso risultato, a meno che una delle proprietà dell’oggetto sia modificata e utilizzata nell’implementazione di equals().
  • L’implementazione del metodo equals() della classe Object restituisce true solo quando entrambi i riferimenti puntano allo stesso oggetto.

Java hashCode()

Il metodo nativo hashCode() dell’oggetto Java restituisce il valore hash code intero dell’oggetto. Il contratto generale del metodo hashCode() è:

  • Diverse invocazioni di hashCode() dovrebbero restituire lo stesso valore intero, a meno che la proprietà dell’oggetto che viene utilizzata nel metodo equals() non sia modificata.
  • Il valore hash code di un oggetto può cambiare in più esecuzioni dello stesso programma.
  • Se due oggetti sono uguali secondo il metodo equals(), allora il loro hash code deve essere lo stesso.
  • Se due oggetti sono diversi secondo il metodo equals(), i loro hash code non devono necessariamente essere diversi. Il loro valore hash code potrebbe essere uguale o diverso.

Importanza del metodo equals() e hashCode()

Il metodo hashCode() e equals() di Java vengono utilizzati nelle implementazioni basate su tabelle hash in Java per memorizzare e recuperare dati. L’ho spiegato dettagliatamente a Come funziona HashMap in Java? L’implementazione di equals() e hashCode() dovrebbe seguire queste regole.

  • Se o1.equals(o2), allora o1.hashCode() == o2.hashCode() dovrebbe sempre essere true.
  • Se o1.hashCode() == o2.hashCode è vero, non significa che o1.equals(o2) sarà true.

Quando sovrascrivere i metodi equals() e hashCode()?

Quando sovrascriviamo il metodo equals(), è quasi necessario sovrascrivere anche il metodo hashCode() in modo che il loro contratto non venga violato dalla nostra implementazione. Nota che il tuo programma non genererà eccezioni se il contratto di equals() e hashCode() viene violato, se non hai intenzione di utilizzare la classe come chiave di una tabella hash, allora non ci saranno problemi. Se hai intenzione di utilizzare una classe come chiave di una tabella hash, è necessario sovrascrivere sia i metodi equals() che hashCode(). Vediamo cosa succede quando ci affidiamo all’implementazione predefinita dei metodi equals() e hashCode() e utilizziamo una classe personalizzata come chiave di una HashMap.

package com.journaldev.java;

public class DataKey {

	private String name;
	private int id;

        // metodi getter e setter

	@Override
	public String toString() {
		return "DataKey [name=" + name + ", id=" + id + "]";
	}

}
package com.journaldev.java;

import java.util.HashMap;
import java.util.Map;

public class HashingTest {

	public static void main(String[] args) {
		Map<DataKey, Integer> hm = getAllData();

		DataKey dk = new DataKey();
		dk.setId(1);
		dk.setName("Pankaj");
		System.out.println(dk.hashCode());

		Integer value = hm.get(dk);

		System.out.println(value);

	}

	private static Map<DataKey, Integer> getAllData() {
		Map<DataKey, Integer> hm = new HashMap<>();

		DataKey dk = new DataKey();
		dk.setId(1);
		dk.setName("Pankaj");
		System.out.println(dk.hashCode());

		hm.put(dk, 10);

		return hm;
	}

}

Quando eseguiamo il programma sopra, verrà stampato null. Questo perché viene utilizzato il metodo hashCode() dell’oggetto Object per trovare il bucket in cui cercare la chiave. Poiché non abbiamo accesso alle chiavi della HashMap e stiamo creando nuovamente la chiave per recuperare i dati, noterai che i valori di hash code di entrambi gli oggetti sono diversi e quindi il valore non viene trovato.

Implementazione dei metodi equals() e hashCode()

Possiamo definire la nostra implementazione dei metodi equals() e hashCode(), ma se non li implementiamo con cura, possono verificarsi problemi strani durante l’esecuzione. Fortunatamente, oggi la maggior parte degli IDE fornisce modi per implementarli automaticamente e, se necessario, possiamo modificarli secondo le nostre esigenze. Possiamo usare Eclipse per generare automaticamente i metodi equals() e hashCode(). Ecco le implementazioni dei metodi equals() e hashCode() generate automaticamente.

@Override
public int hashCode() {
	final int prime = 31;
	int result = 1;
	result = prime * result + id;
	result = prime * result + ((name == null) ? 0 : name.hashCode());
	return result;
}

@Override
public boolean equals(Object obj) {
	if (this == obj)
		return true;
	if (obj == null)
		return false;
	if (getClass() != obj.getClass())
		return false;
	DataKey other = (DataKey) obj;
	if (id != other.id)
		return false;
	if (name == null) {
		if (other.name != null)
			return false;
	} else if (!name.equals(other.name))
		return false;
	return true;
}

Nota che sia i metodi equals() che hashCode() utilizzano gli stessi campi per i calcoli, in modo che il loro contratto rimanga valido. Se eseguirai nuovamente il programma di test, otterremo l’oggetto dalla mappa e il programma stamperà 10. Possiamo anche utilizzare Project Lombok per generare automaticamente le implementazioni dei metodi equals e hashCode.

Cos’è una Collisione Hash

In termini molto semplici, le implementazioni della tabella hash di Java utilizzano la seguente logica per le operazioni get e put.

  1. Prima identificano il “Bucket” da utilizzare utilizzando il codice hash della “chiave”.
  2. Se non ci sono oggetti presenti nel bucket con lo stesso codice hash, aggiungono l’oggetto per l’operazione put e restituiscono null per l’operazione get.
  3. Se ci sono altri oggetti nel bucket con lo stesso codice hash, allora entra in gioco il metodo “equals” della chiave.
    • Se equals() restituisce true ed è un’operazione di put, allora il valore dell’oggetto viene sovrascritto.
    • Se equals() restituisce false ed è un’operazione di put, allora viene aggiunta una nuova voce al bucket.
    • Se equals() restituisce true ed è un’operazione di get, allora viene restituito il valore dell’oggetto.
    • Se equals() restituisce false ed è un’operazione di get, allora viene restituito null.

Nell’immagine sottostante sono mostrati gli elementi di un bucket di HashMap e come sono correlati i metodi equals() e hashCode(). Il fenomeno in cui due chiavi hanno lo stesso codice hash si chiama collisione hash. Se il metodo hashCode() non è implementato correttamente, ci saranno un maggior numero di collisioni hash e le voci della mappa non saranno distribuite correttamente, causando lentezza nelle operazioni di get e put. Questa è la ragione per l’utilizzo di numeri primi nella generazione del codice hash, in modo che le voci della mappa siano distribuite correttamente tra tutti i bucket.

Cosa succede se non implementiamo sia hashCode() che equals()?

Abbiamo già visto sopra che se non viene implementato hashCode(), non saremo in grado di recuperare il valore perché HashMap utilizza il codice hash per trovare il bucket in cui cercare l’entry. Se utilizziamo solo hashCode() e non implementiamo equals(), il valore non verrà comunque recuperato perché il metodo equals() restituirà false.

Pratiche migliori per l’implementazione del metodo equals() e hashCode()

  • Usare le stesse proprietà sia nel metodo equals() che nel metodo hashCode(), in modo che il loro contratto non venga violato quando viene aggiornata una qualsiasi proprietà.
  • È meglio utilizzare oggetti immutabili come chiave della tabella hash in modo che possiamo memorizzare nella cache il codice hash anziché calcolarlo ad ogni chiamata. Ecco perché String è un buon candidato per la chiave della tabella hash perché è immutabile e memorizza nella cache il valore del codice hash.
  • Implementare il metodo hashCode() in modo che si verifichi il minor numero possibile di collisioni di hash e le voci siano distribuite uniformemente su tutti i bucket.

Puoi scaricare il codice completo dal nostro Repository GitHub.

Source:
https://www.digitalocean.com/community/tutorials/java-equals-hashcode