“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:
a == b : false
a.equals(b) : false
a == alias : true
a.equals(alias) : true
literal == literal : true
new String == literal : false
s3.equals(s1) : trueMari kita bedah hasilnya:
a == b : falsedana.equals(b) : false: duanew Bukudengan data identik tetap dua objek berbeda. Operator==menolaknya karena referensinya berbeda;equalsdefault menolaknya karena implementasinya memang mendelegasikan ke perbandingan identitas.a == alias : truedana.equals(alias) : true:Buku alias = atidak menyalin objek, hanya menyalin referensi. Kedua variabel menunjuk satu objek yang sama, sehingga identitas dan nilai sekaligus cocok.literal == literal : true: JVM memakai kembali instance"Java"dari string pool untuk kedua literal, sehinggas1dans2menunjuk objek yang sama.new String == literal : falsenamuns3.equals(s1) : true: objek di luar pool berisi sama tetapi identitasnya beda — bukti kuat memakaiequals()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:
a == b : false
a.equals(b) : true
a.equals(c) : false
a.equals(null) : false
Objects.equals(kosong, null) : trueMari kita bedah implementasi equals baris demi baris:
public boolean equals(Object obj): parameter wajib bertipeObject— tanda tangan inilah yang di-override. Menulisequals(Buku obj)hanya membuat method baru (overload), bukan override, dan collection Java akan tetap memanggil versiObject.if (this == obj): pengoptimalan sekaligus jaminan refleksif — objek dibandingkan dengan dirinya sendiri pasti setara.if (!(obj instanceof Buku lain)): pattern matchinginstanceof(JEP 394) memeriksa tipe sekaligus membinding variabellain. Bandingkan objekBukudenganStringataunullmenghasilkanfalsetanpa exception, terbukti pada outputa.equals(null) : false.Objects.equals(isbn, lain.isbn) && ...: helper darijava.util.Objectsmembandingkan dua referensi dengan aman — salah satunulltidak melemparNullPointerException, dan keduanulldianggap setara.hashCode()berdampingan denganequals: kontrakObjectmensyaratkan objek yang setara memiliki hashCode sama. Tanpa pasangan ini,HashSetdanHashMapakan 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 #
- Oracle API: java.lang.Object (equals dan kontraknya)
- Oracle API: java.util.Objects
- OpenJDK: JEP 394 Pattern Matching for instanceof
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
EqualsNilaiLogis.java
Unduh berkasimport 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 berkasclass 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));
}
}