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:

text
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") : true

Mari kita bedah baris-baris pentingnya:

  1. Baris 6-8 (belanja.add(...)): penyisipan pertama Kopi dan Teh diterima (true), sedangkan Kopi kedua ditolak (false) — dan yang menarik, penolakan tersebut diam-diam: tidak ada exception, cukup nilai balik false yang sering diabaikan pemula.
  2. Baris 9 (size() bernilai 2): ukuran set tetap 2 meski add dipanggil tiga kali — bukti keras bahwa elemen kedua Kopi tidak pernah tersimpan.
  3. Baris 12-13 (new String("Gula") dua kali): operator new menciptakan dua objek berbeda sehingga a == b bernilai false, tetapi a.equals(b) bernilai true. Bagi Set, keduanya adalah nilai yang sama.
  4. Baris 14-15 (add(a) lalu add(b)): a diterima, b ditolak — keunikan bekerja berdasarkan equals, bukan identitas referensi. Ukuran akhir tetap 3 barang.
  5. Baris 19 (contains("Kopi")): pemeriksaan keanggotaan juga memakai equals, bukan ==, sehingga string literal "Kopi" (yang berbagi referensi pool) pun akan cocok dengan Kopi yang 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:

ImplementasiUrutan iterasiKunci keunggulan
HashSettidak dijanjikankecepatan add/contains rata-rata O(1)
LinkedHashSeturutan penyisipankecepatan HashSet + urutan terjaga
TreeSetterurut naik menurut pembandingnavigasi 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 #

Berkas Sumber

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

BuktiDuplikat.java

Unduh berkas
java
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"));
    }
}