“Apakah dua objek ini sama?” ternyata pertanyaan yang ambigu sampai kita menentukan “sama” dalam arti apa. Dua buku dengan ISBN dan judul identik adalah dua objek berbeda di memori, tetapi secara logis keduanya merekam buku yang sama — dan sistem perpustakaan kita mungkin ingin memperlakukan keduanya setara. Java membagi jawaban menjadi dua mekanisme: operator == yang membandingkan identitas (menunjuk objek yang sama atau tidak), dan method equals() yang bisa kita atur untuk membandingkan nilai logis. Pada Chapter ini kita amati keduanya pada domain buku, membongkar perilaku default equals dari Object, lalu menulis override yang benar.

Operator == Membandingkan Referensi #

Untuk tipe primitif, == membandingkan nilai secara langsung: 7 == 7 menghasilkan true. Untuk tipe referensi, maknanya berubah menjadi “kedua variabel menunjuk objek yang sama di heap”. JVM tidak melihat isi objek sama sekali — operator ini hanya membandingkan alamat referensi. Akibatnya, dua objek berisi data persis sama tetap dianggap berbeda oleh ==, karena new menciptakan objek baru di lokasi memori baru.

Kebingungan klasik muncul pada String: literal "Java" yang muncul dua kali berbagi instance di string pool sehingga == menghasilkan true, tetapi new String("Java") membuat objek baru di luar pool sehingga == menghasilkan false. Karena perilaku pool bukan bagian dari logika bisnis, aturan praktisnya: bandingkan isi string selalu dengan equals(), jangan pernah dengan ==.

equals Default dari Object #

Setiap class mewarisi method equals(Object) dari java.lang.Object. Implementasi default-nya membandingkan identitas — sama persis dengan == — sehingga dua objek dengan data serupa tidak akan pernah setara tanpa override. Program berikut membuktikannya:

class Buku {
    private final String judul;
    private final String isbn;

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

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

        // == membandingkan referensi: apakah menunjuk objek yang sama?
        System.out.println("a == b      : " + (a == b));

        // equals default dari Object membandingkan identitas juga.
        System.out.println("a.equals(b) : " + (a.equals(b)));

        // Dua variabel menunjuk objek yang sama: keduanya true.
        Buku alias = a;
        System.out.println("a == alias  : " + (a == alias));
        System.out.println("a.equals(alias) : " + (a.equals(alias)));

        // String literal berbagi instance di string pool.
        String s1 = "Java";
        String s2 = "Java";
        String s3 = new String("Java");
        System.out.println("literal == literal : " + (s1 == s2));
        System.out.println("new String == literal : " + (s3 == s1));
        System.out.println("s3.equals(s1)      : " + s3.equals(s1));
    }
}

Output yang Dihasilkan:

text
a == b      : false
a.equals(b) : false
a == alias  : true
a.equals(alias) : true
literal == literal : true
new String == literal : false
s3.equals(s1)      : true

Mari kita bedah hasilnya:

  1. a == b : false dan a.equals(b) : false: dua new Buku dengan data identik tetap dua objek berbeda. Operator == menolaknya karena referensinya berbeda; equals default menolaknya karena implementasinya memang mendelegasikan ke perbandingan identitas.
  2. a == alias : true dan a.equals(alias) : true: Buku alias = a tidak menyalin objek, hanya menyalin referensi. Kedua variabel menunjuk satu objek yang sama, sehingga identitas dan nilai sekaligus cocok.
  3. literal == literal : true: JVM memakai kembali instance "Java" dari string pool untuk kedua literal, sehingga s1 dan s2 menunjuk objek yang sama.
  4. new String == literal : false namun s3.equals(s1) : true: objek di luar pool berisi sama tetapi identitasnya beda — bukti kuat memakai equals() untuk membandingkan isi string.

Mengoverride equals untuk Nilai Logis #

Ketika domain kita punya konsep “objek yang sama secara logis”, kita menulis ulang equals di dalam class tersebut. Override yang benar memuat tiga pemeriksaan berurutan: Referensi sama → langsung true; tipe atau null → false; bandingkan field yang membentuk identitas logis. Pembandingan field memakai Objects.equals() yang aman terhadap null:

import java.util.Objects;

class Buku {
    private final String judul;
    private final String isbn;

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

    // Override equals(Object): tanda tangan HARUS Object, bukan Buku.
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true; // referensi sama: nilai pasti sama
        }
        if (!(obj instanceof Buku lain)) {
            return false; // tipe lain atau null: tidak setara
        }
        return Objects.equals(isbn, lain.isbn)
                && Objects.equals(judul, lain.judul);
    }

    // Setiap override equals wajib diiringi hashCode yang konsisten.
    // Pembahasan tuntas ada di chapter hashCode dan equals.
    @Override
    public int hashCode() {
        return Objects.hash(isbn, judul);
    }
}

public class EqualsNilaiLogis {
    public static void main(String[] args) {
        Buku a = new Buku("Java Dasar", "978-602-1234-01-5");
        Buku b = new Buku("Java Dasar", "978-602-1234-01-5"); // data sama
        Buku c = new Buku("Java Lanjutan", "978-602-1234-02-2"); // data beda

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

        // Objects.equals aman untuk kedua sisi null.
        Buku kosong = null;
        System.out.println("Objects.equals(kosong, null) : "
                + Objects.equals(kosong, null));
    }
}

Output yang Dihasilkan:

text
a == b          : false
a.equals(b)     : true
a.equals(c)     : false
a.equals(null)  : false
Objects.equals(kosong, null) : true

Mari kita bedah implementasi equals baris demi baris:

  1. public boolean equals(Object obj): parameter wajib bertipe Object — tanda tangan inilah yang di-override. Menulis equals(Buku obj) hanya membuat method baru (overload), bukan override, dan collection Java akan tetap memanggil versi Object.
  2. if (this == obj): pengoptimalan sekaligus jaminan refleksif — objek dibandingkan dengan dirinya sendiri pasti setara.
  3. if (!(obj instanceof Buku lain)): pattern matching instanceof (JEP 394) memeriksa tipe sekaligus membinding variabel lain. Bandingkan objek Buku dengan String atau null menghasilkan false tanpa exception, terbukti pada output a.equals(null) : false.
  4. Objects.equals(isbn, lain.isbn) && ...: helper dari java.util.Objects membandingkan dua referensi dengan aman — salah satu null tidak melempar NullPointerException, dan kedua null dianggap setara.
  5. hashCode() berdampingan dengan equals: kontrak Object mensyaratkan objek yang setara memiliki hashCode sama. Tanpa pasangan ini, HashSet dan HashMap akan berperilaku keliru — Chapter berikutnya membuktikannya.

Dengan equals yang benar, kesetaraan nilai logis kini bisa diuji. Namun di dalam HashSet dan HashMap, pencarian tidak memanggil equals untuk semua elemen — collection mengelompokkan objek berdasarkan hashCode() terlebih dahulu. Bagaimana pasangan ini bekerja dan apa yang terjadi kalau kita mengoverride salah satunya saja? Chapter hashCode dan equals menjawabnya dengan demonstrasi yang sangat praktis.

Referensi #

Berkas Sumber

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

EqualsNilaiLogis.java

Unduh berkas
java
import java.util.Objects;

class Buku {
    private final String judul;
    private final String isbn;

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

    // Override equals(Object): tanda tangan HARUS Object, bukan Buku.
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true; // referensi sama: nilai pasti sama
        }
        if (!(obj instanceof Buku lain)) {
            return false; // tipe lain atau null: tidak setara
        }
        return Objects.equals(isbn, lain.isbn)
                && Objects.equals(judul, lain.judul);
    }

    // Setiap override equals wajib diiringi hashCode yang konsisten.
    // Pembahasan tuntas ada di chapter hashCode dan equals.
    @Override
    public int hashCode() {
        return Objects.hash(isbn, judul);
    }
}

public class EqualsNilaiLogis {
    public static void main(String[] args) {
        Buku a = new Buku("Java Dasar", "978-602-1234-01-5");
        Buku b = new Buku("Java Dasar", "978-602-1234-01-5"); // data sama
        Buku c = new Buku("Java Lanjutan", "978-602-1234-02-2"); // data beda

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

        // Objects.equals aman untuk kedua sisi null.
        Buku kosong = null;
        System.out.println("Objects.equals(kosong, null) : "
                + Objects.equals(kosong, null));
    }
}

IdentitasVsNilai.java

Unduh berkas
java
class Buku {
    private final String judul;
    private final String isbn;

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

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

        // == membandingkan referensi: apakah menunjuk objek yang sama?
        System.out.println("a == b      : " + (a == b));

        // equals default dari Object membandingkan identitas juga.
        System.out.println("a.equals(b) : " + (a.equals(b)));

        // Dua variabel menunjuk objek yang sama: keduanya true.
        Buku alias = a;
        System.out.println("a == alias  : " + (a == alias));
        System.out.println("a.equals(alias) : " + (a.equals(alias)));

        // String literal berbagi instance di string pool.
        String s1 = "Java";
        String s2 = "Java";
        String s3 = new String("Java");
        System.out.println("literal == literal : " + (s1 == s2));
        System.out.println("new String == literal : " + (s3 == s1));
        System.out.println("s3.equals(s1)      : " + s3.equals(s1));
    }
}