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:
get(Pensil) : 120
size() : 3
keys() : Buku Tulis Pensil Penghapus
Penghapus = 30
Buku Tulis = 45
Pensil = 120Poin bedahnya:
- Baris 6-9 (tiga
put): operasi dasar berjalan seperti padaHashMap— kontrakMapdipenuhi, sehingga kode yang hanya memakaiget/put/sizetidak melihat perbedaan. - Baris 13-18 (
Enumeration): loopwhile (kunci.hasMoreElements())dengannextElement()adalah pola era pra-Iterator;Enumerationtetap berfungsi di seluruh API lawas sepertiProperties. - Baris 21-22 (
forEach): method dariMapbekerja karenaHashtablemengimplementasiMapsejak Java 1.2. - Urutan keluaran yang berbeda antara
keys()danforEach: kontrakMapmemang tidak menjanjikan urutan iterasi; padaHashtabledi JDK 25, penelusuranEnumeration/Iteratorberjalan dari bucket terakhir ke pertama, sedangkanforEachdari 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:
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:
Exception in thread "main" java.lang.NullPointerExceptionStack 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:
Hashtable.put synchronized : true
HashMap.put synchronized : false
Superclass Hashtable : java.util.Dictionary
Superclass HashMap : java.util.AbstractMap
Hashtable implements Map : truePoin bedahnya:
- Baris 8-11 (
Modifier.isSynchronized):Hashtable.putterbacatrue,HashMap.putterbacafalse— bukti mekanis bahwa setiap pemanggilanputpadaHashtableharus mengambil monitor lock objek lebih dulu, bahkan ketika program hanya berjalan satu thread. - Baris 13-14 (
getSuperclass()):Hashtablemewarisijava.util.Dictionary— class usang yang tidak dipakai implementasi mana pun yang baru — sedangkanHashMapmewarisiAbstractMapbersamaTreeMapdanLinkedHashMap. - Baris 16-17 (
isAssignableFrom):Hashtabletetap memenuhi kontrakMap, sehingga variabel bertipeMap<String, Integer>dapat diisiHashtablemaupunHashMap— 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.
| Aspek | Hashtable | HashMap | ConcurrentHashMap |
|---|---|---|---|
| Thread-safety | ya, kunci per method | tidak | ya, per-bucket |
null key/value | tidak | ya | tidak |
| Kinerja multi-thread | buruk — objek terkunci penuh | bukan untuk kondisi data berbalap | baik — pembaca tanpa kunci |
| Status | legacy | default single-thread | default 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 #
- Java API Specification: java.util.Hashtable
- Java API Specification: java.util.concurrent.ConcurrentHashMap
- Dev.java: The Java Collections Framework
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
HashtableLama.java
Unduh berkasimport 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 berkasimport 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 berkasimport 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));
}
}