Jika kita membaca kode yang ditulis pada era Java awal — atau menjumpai tutorial lawas di internet — besar kemungkinan kita menemukan Hashtable dipakai sebagai struktur key-value. Class ini lahir bersama Java 1.0 pada tahun 1995, dua tahun sebelum Collections Framework ada, dan sempat menjadi satu-satunya pilihan pemetaan yang tersedia di pustaka standar. Hashtable masih hidup, masih sah dipakai, dan masih berjalan di jutaan baris kode produksi — tetapi posisinya kini berstatus legacy: tidak lagi dikembangkan, dan setiap method publiknya dikunci synchronized demi thread-safety yang harga mahalnya tidak dibutuhkan sebagian besar program. Pada Chapter ini kita jalankan Hashtable sungguhan, buktikan dua perbedaan mencoloknya dari HashMap, lalu kita pahami mengapa migrasinya menuju ConcurrentHashMap.

Sejarah Singkat dan Posisinya Hari Ini #

Hashtable lahir dari class java.util.Dictionary — hierarki pemetaan era Java 1.0 yang kini berstatus usang. Ketika Collections Framework hadir di Java 1.2 (1998) dengan interface Map sebagai kontrak pemetaan baru, Hashtable dipertahankan demi kompatibilitas mundur dan dipretelkan agar ikut mengimplementasi Map — itulah sebabnya Hashtable hari ini punya dua wajah: method baru seperti entrySet dan forEach dari Map, serta method lawas seperti keys() dan elements() yang mengembalikan Enumeration, leluhur interface Iterator.

Rekan satu eranya, Vector, menempuh jalan yang sama: dipretelkan menjadi anggota List. Keduanya kini lebih tepat dibaca sebagai artefak sejarah yang wajib dipahami saat membaca kode lawas, bukan pilihan untuk kode baru.

Hashtable Masih Bisa Dijalankan #

Program berikut memakai Hashtable seperti memakai Map biasa — memasang stok toko, membaca lewat get, lalu mengiterasi dua cara: gaya lawas dengan Enumeration dan gaya modern dengan forEach:

import java.util.*;

public class HashtableLama {
    public static void main(String[] args) {
        Hashtable<String, Integer> stok = new Hashtable<>();
        stok.put("Pensil", 120);
        stok.put("Buku Tulis", 45);
        stok.put("Penghapus", 30);

        System.out.println("get(Pensil) : " + stok.get("Pensil"));
        System.out.println("size()      : " + stok.size());

        // Iterasi gaya lawas: Enumeration, warisan era sebelum Iterator ada.
        Enumeration<String> kunci = stok.keys();
        System.out.print("keys()      : ");
        while (kunci.hasMoreElements()) {
            System.out.print(kunci.nextElement() + " ");
        }
        System.out.println();

        // Iterasi modern tetap berfungsi karena Hashtable kini mengimplementasi Map.
        stok.forEach((barang, jumlah) ->
                System.out.println("  " + barang + " = " + jumlah));
    }
}

Output yang Dihasilkan:

text
get(Pensil) : 120
size()      : 3
keys()      : Buku Tulis Pensil Penghapus 
  Penghapus = 30
  Buku Tulis = 45
  Pensil = 120

Poin bedahnya:

  1. Baris 6-9 (tiga put): operasi dasar berjalan seperti pada HashMap — kontrak Map dipenuhi, sehingga kode yang hanya memakai get/put/size tidak melihat perbedaan.
  2. Baris 13-18 (Enumeration): loop while (kunci.hasMoreElements()) dengan nextElement() adalah pola era pra-Iterator; Enumeration tetap berfungsi di seluruh API lawas seperti Properties.
  3. Baris 21-22 (forEach): method dari Map bekerja karena Hashtable mengimplementasi Map sejak Java 1.2.
  4. Urutan keluaran yang berbeda antara keys() dan forEach: kontrak Map memang tidak menjanjikan urutan iterasi; pada Hashtable di JDK 25, penelusuran Enumeration/Iterator berjalan dari bucket terakhir ke pertama, sedangkan forEach dari pertama ke terakhir — dua urutan berbeda dari tabel yang sama.

Kunci dan Nilai Tidak Boleh Null #

Perbedaan pertama yang sering menjegal migrasi: Hashtable menolak null baik sebagai key maupun value. Berkas berikut sengaja memasang null sebagai value:

java NullDitolak.java
import java.util.*;

public class NullDitolak {
    public static void main(String[] args) {
        Hashtable<String, Integer> stok = new Hashtable<>();
        stok.put("Pensil", 120);
        stok.put("Spidol", null); // Hashtable menolak null sebagai value
        System.out.println("Baris ini tidak pernah tercetak.");
    }
}

JVM menghentikan program dengan exception:

text
Exception in thread "main" java.lang.NullPointerException

Stack trace menunjuk ke dalam java.util.Hashtable.put — pemeriksaan null dilakukan di dalam Hashtable, bukan oleh compiler javac. Sebagai pembanding, HashMap mengizinkan satu key null dan value null sebanyak apa pun, karena pencarian bucket untuk key null diberi perlakuan khusus (hash 0). Perbedaan ini bukan sekadar aturan serampangan: Hashtable bergantung pada key.hashCode() secara langsung, dan memanggil method pada null berarti memicu kegagalan itu juga.

synchronized per Method dan Struktur Warisan #

Perbedaan kedua bersifat arsitektural: seluruh method publik Hashtable diberi modifier synchronized, sehingga hanya satu thread yang boleh menjalankan operasi apa pun pada objek yang sama dalam satu waktu. Program berikut membuktikannya lewat reflection — API Java untuk mengintip struktur class saat program berjalan:

import java.lang.reflect.*;
import java.util.*;

public class StrukturLawas {
    public static void main(String[] args) throws Exception {
        Method putHashtable = Hashtable.class.getMethod("put", Object.class, Object.class);
        Method putHashMap = HashMap.class.getMethod("put", Object.class, Object.class);
        System.out.println("Hashtable.put synchronized : "
                + Modifier.isSynchronized(putHashtable.getModifiers()));
        System.out.println("HashMap.put synchronized   : "
                + Modifier.isSynchronized(putHashMap.getModifiers()));

        System.out.println("Superclass Hashtable : " + Hashtable.class.getSuperclass().getName());
        System.out.println("Superclass HashMap   : " + HashMap.class.getSuperclass().getName());

        System.out.println("Hashtable implements Map : "
                + Map.class.isAssignableFrom(Hashtable.class));
    }
}

Output yang Dihasilkan:

text
Hashtable.put synchronized : true
HashMap.put synchronized   : false
Superclass Hashtable : java.util.Dictionary
Superclass HashMap   : java.util.AbstractMap
Hashtable implements Map : true

Poin bedahnya:

  1. Baris 8-11 (Modifier.isSynchronized): Hashtable.put terbaca true, HashMap.put terbaca false — bukti mekanis bahwa setiap pemanggilan put pada Hashtable harus mengambil monitor lock objek lebih dulu, bahkan ketika program hanya berjalan satu thread.
  2. Baris 13-14 (getSuperclass()): Hashtable mewarisi java.util.Dictionary — class usang yang tidak dipakai implementasi mana pun yang baru — sedangkan HashMap mewarisi AbstractMap bersama TreeMap dan LinkedHashMap.
  3. Baris 16-17 (isAssignableFrom): Hashtable tetap memenuhi kontrak Map, sehingga variabel bertipe Map<String, Integer> dapat diisi Hashtable maupun HashMap — jembatan yang membuat migrasi kode lawas relatif mulus.

Mengapa Migrasi ke ConcurrentHashMap #

Kunci synchronized per-method terdengar aman, tetapi pola ini membuat objek terkunci penuh setiap kali ada satu operasi berjalan. Bayangkan empat kasir mencatat stok ke satu Hashtable yang sama: setiap put dari kasir mana pun membuat tiga kasir lain menunggu, termasuk get yang sebenarnya tidak mengubah apa pun. Skalabilitas utuh dari Hashtable berhenti di satu thread efektif.

AspekHashtableHashMapConcurrentHashMap
Thread-safetyya, kunci per methodtidakya, per-bucket
null key/valuetidakyatidak
Kinerja multi-threadburuk — objek terkunci penuhbukan untuk kondisi data berbalapbaik — pembaca tanpa kunci
Statuslegacydefault single-threaddefault konkuren

ConcurrentHashMap mengambil jalan tengah: bagian-bagian tabel dikunci secara terpisah sehingga penulisan pada bucket yang berbeda berjalan bersamaan, dan pembacaan tidak perlu mengunci sama sekali. Aturan praktisnya ringkas: program satu thread memakai HashMap, program multi-thread memakai ConcurrentHashMap, dan Hashtable hanya muncul saat kita membaca atau merawat kode lama.

Keluarga Map kini tuntas kita jelajahi — dari HashMap yang cepat, LinkedHashMap yang berurutan, TreeMap yang terurut, hingga Hashtable yang bersejarah. Sebelum menutup Section, satu perangkat penghubung masih perlu dibedah: cara collection menelusuri isinya. Iterator dan ListIterator adalah pintu masuknya — sekaligus sumber salah satu exception paling terkenal di Java, yang akan kita picu dan atasi pada Chapter berikutnya.

Referensi #

Berkas Sumber

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

HashtableLama.java

Unduh berkas
java
import java.util.*;

public class HashtableLama {
    public static void main(String[] args) {
        Hashtable<String, Integer> stok = new Hashtable<>();
        stok.put("Pensil", 120);
        stok.put("Buku Tulis", 45);
        stok.put("Penghapus", 30);

        System.out.println("get(Pensil) : " + stok.get("Pensil"));
        System.out.println("size()      : " + stok.size());

        // Iterasi gaya lawas: Enumeration, warisan era sebelum Iterator ada.
        Enumeration<String> kunci = stok.keys();
        System.out.print("keys()      : ");
        while (kunci.hasMoreElements()) {
            System.out.print(kunci.nextElement() + " ");
        }
        System.out.println();

        // Iterasi modern tetap berfungsi karena Hashtable kini mengimplementasi Map.
        stok.forEach((barang, jumlah) ->
                System.out.println("  " + barang + " = " + jumlah));
    }
}

NullDitolak.java

Unduh berkas
java
import java.util.*;

public class NullDitolak {
    public static void main(String[] args) {
        Hashtable<String, Integer> stok = new Hashtable<>();
        stok.put("Pensil", 120);
        stok.put("Spidol", null); // Hashtable menolak null sebagai value
        System.out.println("Baris ini tidak pernah tercetak.");
    }
}

StrukturLawas.java

Unduh berkas
java
import java.lang.reflect.*;
import java.util.*;

public class StrukturLawas {
    public static void main(String[] args) throws Exception {
        Method putHashtable = Hashtable.class.getMethod("put", Object.class, Object.class);
        Method putHashMap = HashMap.class.getMethod("put", Object.class, Object.class);
        System.out.println("Hashtable.put synchronized : "
                + Modifier.isSynchronized(putHashtable.getModifiers()));
        System.out.println("HashMap.put synchronized   : "
                + Modifier.isSynchronized(putHashMap.getModifiers()));

        System.out.println("Superclass Hashtable : " + Hashtable.class.getSuperclass().getName());
        System.out.println("Superclass HashMap   : " + HashMap.class.getSuperclass().getName());

        System.out.println("Hashtable implements Map : "
                + Map.class.isAssignableFrom(Hashtable.class));
    }
}