0Pricing
Java Academy · Lezione

Implementare hashCode

Scrivere funzioni hash corrette

Implementare hashCode è una lezione Java Academy gratuita su CoddyKit. Questa è la lezione 3 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento Java Academy, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso Java Academy include 4 lezioni in totale.

Obiettivi di un buon hashCode

Un buon hashCode() dovrebbe:

  • Restituire lo stesso valore per oggetti uguali (come previsto dal contratto).
  • Distribuire gli oggetti diversi tra molti valori differenti.
  • Essere rapido da calcolare.

Un hashCode scadente che restituisce una costante soddisfa comunque il contratto, ma trasforma la mappa in una lenta lista concatenata.

public class Main {
    public static void main(String[] args) {
        // Legal but terrible: every object collides
        System.out.println("constant hashCode is legal but kills performance");
    }
}

Objects.hash per il caso comune

L'approccio corretto più semplice è Objects.hash(field1, field2, ...).

Gestisce i valori null e combina i campi con un algoritmo standard. Usi gli stessi campi che confronta in equals.

import java.util.Objects;

public class Main {
    static class User {
        final String name; final int age;
        User(String name, int age) { this.name = name; this.age = age; }
        @Override public int hashCode() { return Objects.hash(name, age); }
    }
    public static void main(String[] args) {
        User a = new User("Ada", 36);
        User b = new User("Ada", 36);
        System.out.println(a.hashCode() == b.hashCode());
    }
}

Il moltiplicatore classico 31

Per un hash scritto manualmente, il modello standard moltiplica il risultato corrente per 31 e aggiunge l'hash di ciascun campo.

31 è un numero primo dispari e 31 * x equivale a (x << 5) - x, quindi la JVM può ottimizzarlo.

public class Main {
    static class User {
        final String name; final int age;
        User(String name, int age) { this.name = name; this.age = age; }
        @Override public int hashCode() {
            int result = 17;
            result = 31 * result + (name == null ? 0 : name.hashCode());
            result = 31 * result + age;
            return result;
        }
    }
    public static void main(String[] args) {
        System.out.println(new User("Ada", 36).hashCode());
    }
}

Calcolare l'hash dei tipi primitivi

Ogni tipo primitivo ha un metodo consigliato per il calcolo dell'hash:

  • int: usare direttamente il valore.
  • long: (int)(value ^ (value >>> 32)).
  • boolean: 1 oppure 0.
  • double: Double.hashCode(value).
public class Main {
    public static void main(String[] args) {
        long id = 4_000_000_000L;
        int longHash = (int) (id ^ (id >>> 32));
        System.out.println("long hash: " + longHash);
        System.out.println("double hash: " + Double.hashCode(3.14));
        System.out.println("bool hash: " + Boolean.hashCode(true));
    }
}

Calcolare l'hash degli array

Non chiami direttamente hashCode() su un array: usa l'identità, non il contenuto.

Usi Arrays.hashCode(arr) per un array semplice oppure Arrays.deepHashCode(arr) per array annidati.

import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        int[] a = {1, 2, 3};
        int[] b = {1, 2, 3};
        System.out.println("identity equal: " + (a.hashCode() == b.hashCode()));
        System.out.println("content equal: " + (Arrays.hashCode(a) == Arrays.hashCode(b)));
    }
}

Mantenere equals e hashCode sincronizzati

I campi usati in hashCode() devono essere un sottoinsieme dei campi usati in equals() (idealmente devono essere esattamente gli stessi).

Se equals confronta più campi di hashCode, due oggetti uguali condividono comunque lo stesso hash. È consentito. Ma se hashCode usa un campo ignorato da equals, il contratto viene violato.

import java.util.Objects;

public class Main {
    static class Coord {
        final int x, y;
        Coord(int x, int y) { this.x = x; this.y = y; }
        @Override public boolean equals(Object o) {
            return o instanceof Coord c && c.x == x && c.y == y;
        }
        @Override public int hashCode() { return Objects.hash(x, y); }
    }
    public static void main(String[] args) {
        Coord a = new Coord(3, 4), b = new Coord(3, 4);
        System.out.println(a.equals(b) && a.hashCode() == b.hashCode());
    }
}

Memorizzare l'hash nella cache

Per oggetti immutabili il cui hash è costoso da calcolare, può memorizzare il risultato in un campo.

String fa esattamente questo internamente. Lo faccia solo quando l'oggetto è realmente immutabile, così il valore memorizzato non diventa mai obsoleto.

import java.util.Objects;

public class Main {
    static final class Key {
        final String a, b;
        private int hash; // 0 until computed
        Key(String a, String b) { this.a = a; this.b = b; }
        @Override public int hashCode() {
            int h = hash;
            if (h == 0) { h = Objects.hash(a, b); hash = h; }
            return h;
        }
    }
    public static void main(String[] args) {
        Key k = new Key("x", "y");
        System.out.println(k.hashCode());
        System.out.println(k.hashCode());
    }
}

La distribuzione è importante

Un hashCode ben distribuito distribuisce le chiavi uniformemente tra i bucket. Contiamo i codici hash distinti di un gruppo di oggetti.

Più valori distinti ci sono, meno collisioni si verificano e più veloce è la mappa.

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

public class Main {
    record Pair(int a, int b) {}
    public static void main(String[] args) {
        Set<Integer> hashes = new HashSet<>();
        for (int i = 0; i < 100; i++) {
            hashes.add(Objects.hash(i, i * 7));
        }
        System.out.println("distinct hashes: " + hashes.size());
    }
}

Un esempio di distribuzione errata

Sommare i campi senza moltiplicare produce collisioni: (1,2) e (2,1) producono entrambi l'hash 3.

Il moltiplicatore 31 elimina questa simmetria, perché in questo modo l'ordine diventa importante.

public class Main {
    static int badHash(int a, int b) { return a + b; }
    static int goodHash(int a, int b) { return 31 * a + b; }
    public static void main(String[] args) {
        System.out.println("bad (1,2): " + badHash(1, 2) + ", (2,1): " + badHash(2, 1));
        System.out.println("good (1,2): " + goodHash(1, 2) + ", (2,1): " + goodHash(2, 1));
    }
}

Preferire i record per i tipi valore

Per i semplici contenitori di dati, un record genera automaticamente un hashCode corretto e ben distribuito.

Scriva hashCode manualmente solo quando ha bisogno di una semantica personalizzata o non può usare un record.

public class Main {
    record Money(long cents, String currency) {}
    public static void main(String[] args) {
        Money a = new Money(1099, "USD");
        Money b = new Money(1099, "USD");
        System.out.println(a.equals(b));
        System.out.println(a.hashCode() == b.hashCode());
    }
}

Mettere tutto insieme

Una classe valore completa: campi immutabili, equals e hashCode basati sugli stessi campi e un toString pulito.

import java.util.Objects;

public class Main {
    static final class Version {
        final int major, minor, patch;
        Version(int major, int minor, int patch) {
            this.major = major; this.minor = minor; this.patch = patch;
        }
        @Override public boolean equals(Object o) {
            return o instanceof Version v && v.major == major && v.minor == minor && v.patch == patch;
        }
        @Override public int hashCode() { return Objects.hash(major, minor, patch); }
        @Override public String toString() { return major + "." + minor + "." + patch; }
    }
    public static void main(String[] args) {
        Version v = new Version(2, 1, 0);
        System.out.println(v + " hash=" + v.hashCode());
    }
}

Verifica rapida

Verifichi le Sue competenze su hashCode.

Riepilogo

Ha imparato a implementare correttamente hashCode:

  • Usare Objects.hash(...) per il caso comune.
  • Il modello del moltiplicatore 31 per il calcolo manuale dell'hash.
  • Calcolare l'hash degli array con Arrays.hashCode, non con il metodo predefinito.
  • Mantenere sincronizzati i campi di hashCode con equals e preferire i record.

Successivamente vedrà come Java 8+ converte in alberi i bucket sovraccarichi.

import java.util.Objects;

public class Main {
    public static void main(String[] args) {
        System.out.println("hashCode recap done: " + Objects.hash("done"));
    }
}

Domande Frequenti

La lezione «Implementare hashCode» è gratuita?

Sì — il testo completo di «Implementare hashCode» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso Java Academy, passa a CoddyKit PRO. Il corso Java Academy include 4 lezioni in totale.

Cosa imparerò in «Implementare hashCode»?

Scrivere funzioni hash corrette Eserciti Java Academy con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.

Ho bisogno di esperienza per iniziare Java Academy?

Non è richiesta alcuna esperienza precedente. Java Academy su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 3 di 4.

Quanto tempo richiede la lezione «Implementare hashCode»?

La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.

Posso scrivere ed eseguire codice in questa lezione Java Academy?

Sì. Ogni lezione Java Academy include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.

Tutte le lezioni di questo corso

  1. Come funziona HashMap
  2. Il contratto di equals/hashCode
  3. Implementare hashCode
  4. Treeification e prestazioni
← Torna a Java Academy