Bayangkan sebuah daftar belanja yang tidak sengaja menulis “Kopi” tiga kali, atau daftar peserta rapat yang mencatat satu orang dua kali karena pendaftaran ulang. Kesalahan semacam itu muncul terus-menerus di dunia nyata, dan kontrak Set hadir tepat untuk mencegahnya di tingkat struktur data: Set adalah Collection yang tidak pernah menyimpan duplikat. Keputusan “duplikat atau tidak” tidak diambil oleh nomor posisi seperti pada List, melainkan oleh kesetaraan nilai lewat method equals dan hashCode. Pada Chapter ini kita buktikan perilaku penolakan duplikat lewat output program sungguhan, lalu kita pahami mekanisme di balik keputusan tersebut.
Kontrak Elemen Unik #
Dokumentasi Set mendefinisikan duplikat secara formal: sebuah Set tidak boleh memuat dua elemen e1 dan e2 yang memenuhi e1.equals(e2). Konsekuensi pertamanya, Set paling menyimpan satu null (karena null hanya setara dengan null itu sendiri). Konsekuensi kedua lebih penting: keunikan dilihat dari nilai menurut equals, bukan dari identitas objek. Dua objek berbeda di memori yang nilainya sama tetap dianggap duplikat.
Kontrak ini juga mengubah wajah method yang diwarisi dari Collection. Method add(e) pada List selalu menambahkan dan mengembalikan true; pada Set, method tersebut mengembalikan boolean yang bermakna diagnostik — true berarti elemen benar-benar masuk, false berarti elemen ditolak karena sudah ada. Karakteristik Set lain yang perlu diingat: tidak ada get(indeks), tidak ada posisi manapun — struktur ini murni soal keanggotaan, sehingga pertanyaan yang cocok untuk Set adalah “apakah X sudah ada?”, bukan “elemen ke berapa X?”.
Bukti Output Penolakan Duplikat #
Program berikut mengisi sebuah HashSet dengan nama-nama barang belanja, termasuk beberapa upaya penyisipan ganda yang sengaja dibuat. Nilai kembalian add dicetak setiap kali agar keputusan penerimaan atau penolakan terlihat langsung:
import java.util.*;
public class BuktiDuplikat {
public static void main(String[] args) {
Set<String> belanja = new HashSet<>();
System.out.println("add(\"Kopi\") : " + belanja.add("Kopi"));
System.out.println("add(\"Teh\") : " + belanja.add("Teh"));
System.out.println("add(\"Kopi\") lagi : " + belanja.add("Kopi"));
System.out.println("Isi : " + belanja + " (ukuran " + belanja.size() + ")");
// Dua objek String berbeda yang bernilai sama.
String a = new String("Gula");
String b = new String("Gula");
System.out.println("a == b : " + (a == b));
System.out.println("a.equals(b) : " + a.equals(b));
System.out.println("add(a) : " + belanja.add(a));
System.out.println("add(b) : " + belanja.add(b));
System.out.println("Isi : " + belanja + " (ukuran " + belanja.size() + ")");
// contains memakai equals, bukan identitas referensi.
System.out.println("contains(\"Kopi\") : " + belanja.contains("Kopi"));
}
}Output yang Dihasilkan:
add("Kopi") : true
add("Teh") : true
add("Kopi") lagi : false
Isi : [Teh, Kopi] (ukuran 2)
a == b : false
a.equals(b) : true
add(a) : true
add(b) : false
Isi : [Gula, Teh, Kopi] (ukuran 3)
contains("Kopi") : trueMari kita bedah baris-baris pentingnya:
- Baris 6-8 (
belanja.add(...)): penyisipan pertamaKopidanTehditerima (true), sedangkanKopikedua ditolak (false) — dan yang menarik, penolakan tersebut diam-diam: tidak ada exception, cukup nilai balikfalseyang sering diabaikan pemula. - Baris 9 (
size()bernilai 2): ukuran set tetap 2 meskiadddipanggil tiga kali — bukti keras bahwa elemen keduaKopitidak pernah tersimpan. - Baris 12-13 (
new String("Gula")dua kali): operatornewmenciptakan dua objek berbeda sehinggaa == bbernilaifalse, tetapia.equals(b)bernilaitrue. BagiSet, keduanya adalah nilai yang sama. - Baris 14-15 (
add(a)laluadd(b)):aditerima,bditolak — keunikan bekerja berdasarkanequals, bukan identitas referensi. Ukuran akhir tetap 3 barang. - Baris 19 (
contains("Kopi")): pemeriksaan keanggotaan juga memakaiequals, bukan==, sehingga string literal"Kopi"(yang berbagi referensi pool) pun akan cocok denganKopiyang sudah tersimpan.
Peran equals dan hashCode #
Bagaimana implementasi seperti HashSet bisa memutuskan “sudah ada atau belum” tanpa memeriksa seluruh isi? Jawabannya adalah kerja sama dua method. Method hashCode mengubah setiap objek menjadi sebuah angka, dan HashSet menggunakan angka tersebut untuk memilih bucket (wadah kelompok) tempat objek diletakkan — dua objek yang setara akan selalu jatuh di bucket yang sama karena kontraknya menuntut objek yang equals harus punya hashCode yang sama. Setelah berada di bucket yang sama, barulah equals dipanggil untuk memastikan kesetaraan yang sesungguhnya.
Dari mekanisme tersebut lahir aturan penting saat kita membuat class sendiri. Jika equals dioverride tanpa hashCode, dua objek yang “sama” bisa mendapat hashCode berbeda, berakhir di bucket berbeda, dan Set akan menyimpan keduanya sebagai dua elemen terpisah — duplikat lolos tanpa peringatan. Chapter HashCode dan Equals di Section sebelumnya membahas kontrak dan cara override keduanya secara benar untuk class buatan sendiri.
Tiga Wajah Implementasi Set #
Kontrak Set menuntut keunikan, tetapi urutan iterasi dan performa diserahkan kepada implementasi. Tabel berikut merangkum tiga implementasi utama yang akan kita bahas pada chapter-chapter berikutnya:
| Implementasi | Urutan iterasi | Kunci keunggulan |
|---|---|---|
HashSet | tidak dijanjikan | kecepatan add/contains rata-rata O(1) |
LinkedHashSet | urutan penyisipan | kecepatan HashSet + urutan terjaga |
TreeSet | terurut naik menurut pembanding | navigasi first/last/floor/ceiling |
Ketiganya memakai equals dan hashCode dengan bobot berbeda: HashSet dan LinkedHashSet bergantung penuh pada keduanya, sedangkan TreeSet menilai keunikan lewat perbandingan compareTo/Comparator — cukup satu angka nol untuk menyatakan “duplikat”. Pemilihan di antara ketiganya adalah materi tiga chapter berikutnya, dimulai dari yang paling cepat dan paling banyak dipakai: HashSet.
Referensi #
- Java API Specification: java.util.Set
- Java API Specification: java.util.Collection
- Dev.java: The Java Collections Framework
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
BuktiDuplikat.java
Unduh berkasimport java.util.*;
public class BuktiDuplikat {
public static void main(String[] args) {
Set<String> belanja = new HashSet<>();
System.out.println("add(\"Kopi\") : " + belanja.add("Kopi"));
System.out.println("add(\"Teh\") : " + belanja.add("Teh"));
System.out.println("add(\"Kopi\") lagi : " + belanja.add("Kopi"));
System.out.println("Isi : " + belanja + " (ukuran " + belanja.size() + ")");
// Dua objek String berbeda yang bernilai sama.
String a = new String("Gula");
String b = new String("Gula");
System.out.println("a == b : " + (a == b));
System.out.println("a.equals(b) : " + a.equals(b));
System.out.println("add(a) : " + belanja.add(a));
System.out.println("add(b) : " + belanja.add(b));
System.out.println("Isi : " + belanja + " (ukuran " + belanja.size() + ")");
// contains memakai equals, bukan identitas referensi.
System.out.println("contains(\"Kopi\") : " + belanja.contains("Kopi"));
}
}