Chapter sebelumnya menutup dengan janji: kegagalan mengoverride hashCode akan terlihat nyata pada collection. HashSet dan HashMap memang tidak membandingkan seluruh elemen satu per satu dengan equals — collection tersebut mengelompokkan objek ke dalam bucket berdasarkan hashCode(), lalu memanggil equals hanya pada objek dalam bucket yang sama. Mekanisme dua tahap inilah yang membuat pencarian hash begitu cepat, sekaligus menjadi sumber bug paling senyap di Java: equals yang dioverride tanpa hashCode yang mengikuti membuat objek “hilang” dari HashSet. Pada Chapter ini kita membuktikan kegagalan tersebut secara langsung, lalu memperbaikinya dan memahami kontrak yang mengikat kedua method.

Bagaimana HashSet Mencari Objek #

Saat rak.add(buku) dipanggil, HashSet menghitung buku.hashCode() untuk menentukan bucket penyimpanan. Saat rak.contains(cari) dipanggil, HashSet menghitung cari.hashCode(), menuju bucket yang ditunjuk nilai tersebut, lalu membandingkan kandidat di dalamnya dengan equals. Pencarian berhasil hanya jika hash objek yang dicari mengantarkan ke bucket yang tepat dan equals mengonfirmasi kecocokan.

Masalah muncul karena hashCode default dari Object memakai identitas objek, sehingga dua objek berbeda nyaris selalu berhash beda. contains() pun mencari di bucket yang salah dan melaporkan false tanpa pernah memanggil equals — objek tetap ada di dalam set, jalur pencariannya saja yang tidak sampai.

HashSet Kehilangan Objek Tanpa hashCode #

Program berikut mendefinisikan equals berbasis ISBN — dua buku dengan ISBN sama dianggap setara — tetapi sengaja membiarkan hashCode tetap memakai versi Object:

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

class Buku {
    private final String isbn;

    public Buku(String isbn) {
        this.isbn = isbn;
    }

    // equals dioverride: dua Buku dengan ISBN sama dianggap setara.
    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Buku lain)) {
            return false;
        }
        return isbn.equals(lain.isbn);
    }

    // hashCode sengaja TIDAK dioverride: masih versi default dari Object.
}

public class SetTanpaHashCode {
    public static void main(String[] args) {
        Buku anggota = new Buku("978-602-1234-01-5");
        Set<Buku> rak = new HashSet<>();
        rak.add(anggota);

        Buku dicari = new Buku("978-602-1234-01-5");

        // equals bilang setara...
        System.out.println("dicari.equals(anggota) : " + dicari.equals(anggota));

        // ...tetapi HashSet tidak menemukannya.
        System.out.println("rak.contains(dicari)   : " + rak.contains(dicari));

        // add objek "setara" malah menambah elemen kedua.
        rak.add(dicari);
        System.out.println("rak.size() setelah add : " + rak.size());
    }
}

Output yang Dihasilkan:

text
dicari.equals(anggota) : true
rak.contains(dicari)   : false
rak.size() setelah add : 2

Output tersebut adalah diagnosa yang instruktif. dicari.equals(anggota) menghasilkan true — method kita bekerja. Namun rak.contains(dicari) menghasilkan false: HashSet menghitung hash dicari berbasis identitas, menuju bucket yang berbeda, dan tidak menemukan kandidat untuk dibandingkan. Lebih parah, rak.add(dicari) menambah elemen kedua — set kini berisi dua buku yang menurut equals adalah buku yang sama. HashMap menderita penyakit yang sama: get() dengan key “setara” mengembalikan null.

Kontrak hashCode dan equals #

Spesifikasi Object mengikat kedua method dengan kontrak yang tidak bisa ditawar:

  • Objek setara wajib berhash sama: jika a.equals(b) menghasilkan true, maka a.hashCode() == b.hashCode() harus true. Inilah aturan yang dilanggar contoh di atas.
  • Objek tidak setara boleh berhash sama: kebalikannya tidak diwajibkan. Dua objek dengan hash sama (collision) tetap dibedakan lewat equals — collision hanya memperlambat, bukan mengacaukan.
  • Hasil konsisten selama objek tidak berubah: pemanggilan berulang hashCode() pada objek yang sama harus mengembalikan nilai konsisten dalam satu eksekusi program.
  • Basis data yang sama: hitung equals dan hashCode dari field-field yang identik — dalam contoh kita, keduanya berbasis isbn.

Aturan pertama menjelaskan mengapa override equals saja tidak pernah cukup: begitu dua objek berbeda dinyatakan setara, hash keduanya wajib disamakan, dan itu hanya bisa dilakukan dengan mengoverride hashCode.

Memperbaiki dengan hashCode Override #

Perbaikannya satu baris method berbasis field yang sama dengan equals. Helper Objects.hash(...) menghitung gabungan hash dari seluruh field yang kita berikan:

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

class Buku {
    private final String isbn;

    public Buku(String isbn) {
        this.isbn = isbn;
    }

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Buku lain)) {
            return false;
        }
        return isbn.equals(lain.isbn);
    }

    // hashCode kini konsisten dengan equals: basisnya field yang sama.
    @Override
    public int hashCode() {
        return Objects.hash(isbn);
    }
}

public class SetDenganHashCode {
    public static void main(String[] args) {
        Buku a = new Buku("978-602-1234-01-5");
        Buku b = new Buku("978-602-1234-01-5");

        System.out.println("a.equals(b)                 : " + a.equals(b));
        System.out.println("a.hashCode() == b.hashCode(): "
                + (a.hashCode() == b.hashCode()));
        System.out.println("a.hashCode()                : " + a.hashCode());

        Set<Buku> rak = new HashSet<>();
        rak.add(a);
        System.out.println("rak.contains(b)             : " + rak.contains(b));

        rak.add(b);
        System.out.println("setelah add b, rak.size()   : " + rak.size());

        Map<Buku, String> lokasi = new HashMap<>();
        lokasi.put(a, "Rak A-1");
        System.out.println("lokasi.get(b)               : " + lokasi.get(b));
    }
}

Output yang Dihasilkan:

text
a.equals(b)                 : true
a.hashCode() == b.hashCode(): true
a.hashCode()                : 518617003
rak.contains(b)             : true
setelah add b, rak.size()   : 1
lokasi.get(b)               : Rak A-1

Mari kita bedah hasil yang kini sehat:

  1. a.equals(b) : true dan a.hashCode() == b.hashCode() : true: kontrak terpenuhi — objek setara berhash sama. Nilai 518617003 berasal dari String.hashCode() atas ISBN yang sama.
  2. rak.contains(b) : true: b.hashCode() mengantarkan HashSet ke bucket tempat a tersimpan, equals mengonfirmasi, dan pencarian berhasil.
  3. setelah add b, rak.size() : 1: add() menemukan duplikat melalui jalur hash-then-equals, sehingga elemen kedua ditolak. Set kini benar-benar berisi nilai unik.
  4. lokasi.get(b) : Rak A-1: HashMap mengambil entry yang dipasang lewat key a dengan key b — perilaku yang kita harapkan dari key “setara”.

Kontrak equals/hashCode kini lengkap: nilai logis, hash, dan kerja sama collection berjalan selaras. Pertanyaan berikutnya menyentuh penyalinan objek: bagaimana membuat duplikat objek yang benar-benar terpisah dari aslinya? Chapter Object Cloning membedah clone() beserta jebakan salinan dangkalnya, lalu menawarkan alternatif yang lebih aman.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

SetDenganHashCode.java

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

class Buku {
    private final String isbn;

    public Buku(String isbn) {
        this.isbn = isbn;
    }

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Buku lain)) {
            return false;
        }
        return isbn.equals(lain.isbn);
    }

    // hashCode kini konsisten dengan equals: basisnya field yang sama.
    @Override
    public int hashCode() {
        return Objects.hash(isbn);
    }
}

public class SetDenganHashCode {
    public static void main(String[] args) {
        Buku a = new Buku("978-602-1234-01-5");
        Buku b = new Buku("978-602-1234-01-5");

        System.out.println("a.equals(b)                 : " + a.equals(b));
        System.out.println("a.hashCode() == b.hashCode(): "
                + (a.hashCode() == b.hashCode()));
        System.out.println("a.hashCode()                : " + a.hashCode());

        Set<Buku> rak = new HashSet<>();
        rak.add(a);
        System.out.println("rak.contains(b)             : " + rak.contains(b));

        rak.add(b);
        System.out.println("setelah add b, rak.size()   : " + rak.size());

        Map<Buku, String> lokasi = new HashMap<>();
        lokasi.put(a, "Rak A-1");
        System.out.println("lokasi.get(b)               : " + lokasi.get(b));
    }
}

SetTanpaHashCode.java

Unduh berkas
java
import java.util.HashSet;
import java.util.Set;

class Buku {
    private final String isbn;

    public Buku(String isbn) {
        this.isbn = isbn;
    }

    // equals dioverride: dua Buku dengan ISBN sama dianggap setara.
    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Buku lain)) {
            return false;
        }
        return isbn.equals(lain.isbn);
    }

    // hashCode sengaja TIDAK dioverride: masih versi default dari Object.
}

public class SetTanpaHashCode {
    public static void main(String[] args) {
        Buku anggota = new Buku("978-602-1234-01-5");
        Set<Buku> rak = new HashSet<>();
        rak.add(anggota);

        Buku dicari = new Buku("978-602-1234-01-5");

        // equals bilang setara...
        System.out.println("dicari.equals(anggota) : " + dicari.equals(anggota));

        // ...tetapi HashSet tidak menemukannya.
        System.out.println("rak.contains(dicari)   : " + rak.contains(dicari));

        // add objek "setara" malah menambah elemen kedua.
        rak.add(dicari);
        System.out.println("rak.size() setelah add : " + rak.size());
    }
}