Java Map: Panduan Lengkap dari HashMap hingga ConcurrentHashMap
Oct 2026 · 6 min read
Panduan praktis memahami interface Map di Java: perbandingan HashMap, LinkedHashMap, TreeMap, dan ConcurrentHashMap, method modern Java 8+ seperti computeIfAbsent dan merge, serta jebakan umum yang sering terjadi di produksi.
Hampir setiap aplikasi Java butuh menyimpan data berpasangan key-value: konfigurasi, cache, hitungan request, pemetaan ID ke objek, dan masih banyak lagi. Semua itu berpusat pada satu interface: java.util.Map. Artikel ini membahas cara pakai yang benar, perbedaan tiap implementasi, method modern yang membuat kode jauh lebih ringkas, sampai jebakan yang paling sering memakan korban.
Apa Itu Map?
Map<K, V> adalah struktur data yang memetakan key unik ke sebuah value. Berbeda dengan Collection, Map bukan turunan dari Collection dan tidak memiliki iterator() langsung — iterasi dilakukan lewat view seperti entrySet(), keySet(), atau values().
Tiga aturan penting yang wajib diingat:
Key harus unik.
put()dengan key yang sama akan menimpa value lama dan mengembalikan value lama tersebut.Pencarian bergantung pada
equals()danhashCode()dari key.Urutan elemen tergantung implementasi —
HashMaptidak menjamin apa pun.
Implementasi yang Perlu Diketahui
Implementasi | Urutan | Null key | Null value | get/put | Thread-safe |
|---|---|---|---|---|---|
| Tidak dijamin | 1 | Ya | O(1) rata-rata | Tidak |
| Insertion / access order | 1 | Ya | O(1) rata-rata | Tidak |
| Terurut (sorted) | Tidak | Ya | O(log n) | Tidak |
| Tidak dijamin | Tidak | Tidak | O(1) rata-rata | Ya (legacy) |
| Tidak dijamin | Tidak | Tidak | O(1) rata-rata | Ya |
Aturan praktis: default ke
HashMap. GunakanLinkedHashMapkalau urutan penting,TreeMapkalau butuh operasi range/sorted, danConcurrentHashMapkalau diakses banyak thread.Hashtablesebaiknya tidak dipakai lagi untuk kode baru.
Operasi Dasar
javaimport java.util.HashMap;
import java.util.Map;
Map<String, Integer> stok = new HashMap<>();
// Menambah atau menimpa value
stok.put("apel", 10);
stok.put("jeruk", 5);
// Mengambil value; get() mengembalikan null jika key tidak ada
Integer apel = stok.get("apel"); // 10
int mangga = stok.getOrDefault("mangga", 0); // 0, aman dari NPE
// Memeriksa keberadaan
boolean ada = stok.containsKey("jeruk"); // true
boolean kosong = stok.isEmpty(); // false
// Menghapus
stok.remove("jeruk");
// Menimpa value hanya jika key sudah ada
stok.replace("apel", 20);
System.out.println(stok.size()); // 1
System.out.println(stok); // {apel=20}
Perhatikan: get() mengembalikan null baik ketika key tidak ditemukan maupun ketika value-nya memang null. Kalau kedua kondisi itu perlu dibedakan, gunakan containsKey() atau tipe primitif wrapper yang tidak mengizinkan null.
Cara Iterasi yang Benar
javaMap<String, Integer> stok = Map.of("apel", 10, "jeruk", 5);
// 1. entrySet() — paling efisien karena key dan value diambil sekaligus
for (Map.Entry<String, Integer> entry : stok.entrySet()) {
System.out.println(entry.getKey() + " = " + entry.getValue());
}
// 2. forEach — gaya fungsional (Java 8+)
stok.forEach((k, v) -> System.out.println(k + " = " + v));
// 3. keySet() — jika hanya butuh key
for (String key : stok.keySet()) {
System.out.println(key);
}
// 4. values() — jika hanya butuh value
for (Integer value : stok.values()) {
System.out.println(value);
}
Hindari pola for (String key : map.keySet()) { map.get(key); } — Anda melakukan dua kali pencarian hash padahal satu iterasi entrySet() sudah cukup.
Method Modern Java 8+ yang Wajib Dikuasai
Sebelum Java 8, menambahkan nilai ke list di dalam map selalu berakhir dengan kode if (map.get(k) == null) yang berulang. Sekarang semuanya bisa satu baris.
javaMap<String, Integer> stok = new HashMap<>();
// putIfAbsent: hanya menulis kalau key belum ada
stok.putIfAbsent("apel", 10);
// computeIfAbsent: buat value baru hanya jika key belum ada
Map<String, List<String>> grup = new HashMap<>();
grup.computeIfAbsent("buah", k -> new ArrayList<>()).add("apel");
grup.computeIfAbsent("buah", k -> new ArrayList<>()).add("jeruk");
// {buah=[apel, jeruk]}
// compute: hitung ulang berdasarkan nilai lama
stok.compute("apel", (k, v) -> v == null ? 1 : v + 1);
// merge: gabungkan nilai lama dan nilai baru
stok.merge("apel", 3, Integer::sum); // 11 + 3 = 14
// replaceAll: transformasi semua value
stok.replaceAll((k, v) -> v * 2);
merge() adalah senjata utama untuk menghitung frekuensi:
javaMap<String, Integer> frekuensi = new HashMap<>();
for (String kata : List.of("apel", "jeruk", "apel", "mangga", "apel")) {
frekuensi.merge(kata, 1, Integer::sum);
}
// {apel=3, jeruk=1, mangga=1}
Map dengan Objek sebagai Key
Ini sumber bug paling umum: custom key yang lupa meng-override equals() dan hashCode(). Akibatnya, dua objek yang secara logika sama dianggap berbeda.
java// Cara terbaik: gunakan record (Java 16+), equals/hashCode otomatis
record Karyawan(String id) {}
Map<Karyawan, String> divisi = new HashMap<>();
divisi.put(new Karyawan("E001"), "Engineering");
System.out.println(divisi.get(new Karyawan("E001"))); // "Engineering"
Kalau memakai class biasa, implementasi minimumnya seperti ini:
javapublic final class Karyawan {
private final String id;
public Karyawan(String id) { this.id = id; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Karyawan)) return false;
return id.equals(((Karyawan) o).id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
Pastikan field yang dipakai di hashCode() bersifat immutable. Kalau field berubah setelah key dimasukkan ke map, entry tersebut praktis "hilang" karena hash-nya menunjuk ke bucket yang berbeda.
Map Immutable dan Pembuatan Cepat
java// Kosong
Map<String, Integer> kosong = Map.of();
// Maksimal 10 pasangan
Map<String, Integer> tetap = Map.of("apel", 10, "jeruk", 5);
// Lebih dari 10 pasangan
Map<String, Integer> banyak = Map.ofEntries(
Map.entry("apel", 10),
Map.entry("jeruk", 5),
Map.entry("mangga", 7)
);
// Salinan immutable dari map yang sudah ada
Map<String, Integer> salinan = Map.copyOf(banyak);
// Balik ke versi mutable
Map<String, Integer> mutable = new HashMap<>(tetap);
Map hasil Map.of() akan melempar UnsupportedOperationException jika dimodifikasi. Ini fitur, bukan bug — data jadi aman dibagikan antar thread tanpa sinkronisasi.
TreeMap: Saat Butuh Urutan
javaTreeMap<String, Integer> skor = new TreeMap<>();
skor.put("budi", 80);
skor.put("ani", 95);
skor.put("cita", 70);
System.out.println(skor.firstKey()); // ani
System.out.println(skor.lastKey()); // cita
System.out.println(skor.headMap("budi")); // {ani=95}
System.out.println(skor.subMap("ani", "cita")); // {ani=95, budi=80}
System.out.println(skor.descendingMap()); // {cita=70, budi=80, ani=95}
TreeMap mengurutkan berdasarkan Comparable natural dari key atau Comparator yang diberikan di konstruktor. Trade-off-nya: setiap operasi menjadi O(log n), dan null key tidak diizinkan karena akan melempar NullPointerException.
LinkedHashMap sebagai LRU Cache Sederhana
Dengan mengaktifkan access order, LinkedHashMap bisa dipakai langsung sebagai cache dengan eviction policy LRU tanpa library tambahan.
javapublic class LruCache<K, V> extends LinkedHashMap<K, V> {
private final int kapasitas;
public LruCache(int kapasitas) {
super(kapasitas, 0.75f, true); // accessOrder = true
this.kapasitas = kapasitas;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > kapasitas;
}
}
// Pemakaian
LruCache<String, String> cache = new LruCache<>(2);
cache.put("a", "1");
cache.put("b", "2");
cache.get("a"); // "a" jadi paling baru dipakai
cache.put("c", "3"); // "b" terbuang
System.out.println(cache); // {a=1, c=3}
ConcurrentHashMap untuk Akses Multi-thread
javaConcurrentHashMap<String, Integer> hits = new ConcurrentHashMap<>();
// AMAN: operasi atomik, tidak ada race condition
hits.merge("/home", 1, Integer::sum);
hits.computeIfAbsent("/api", k -> 0);
// hitung nilai lama lalu ganti, atomik
hits.compute("/home", (k, v) -> v == null ? 1 : v + 1);
Jangan lakukan pola check-then-act pada map yang dipakai bersama:
java// SALAH: race condition antara containsKey dan put
if (!hits.containsKey("/home")) {
hits.put("/home", 0);
}
// BENAR
hits.putIfAbsent("/home", 0);
Sejak Java 8, ConcurrentHashMap menggunakan CAS dan sinkronisasi per-bucket, bukan lock seluruh map. Ini membuatnya jauh lebih cepat daripada Hashtable atau Collections.synchronizedMap(). Catatan penting: null key dan null value tidak diizinkan.
Pola Umum: Grouping dan Nested Map
javaList<String> kata = List.of("apel", "anggur", "jeruk", "jambu", "alpukat");
Map<Character, List<String>> perHuruf = kata.stream()
.collect(Collectors.groupingBy(k -> k.charAt(0)));
// {a=[apel, anggur, alpukat], j=[jeruk, jambu]}
// Map bersarang: kota -> (produk -> jumlah)
Map<String, Map<String, Integer>> penjualan = new HashMap<>();
penjualan.computeIfAbsent("Jakarta", k -> new HashMap<>())
.merge("Kopi", 3, Integer::sum);
Menggabungkan computeIfAbsent() dan merge() membuat nested map jauh lebih rapi dibanding puluhan null check.
Jebakan yang Sering Terjadi
Mutasi key setelah dimasukkan. Hash berubah, entry tidak bisa ditemukan lagi. Gunakan key immutable.
Modifikasi map saat iterasi.
for (var e : map.entrySet()) map.remove(e.getKey());melemparConcurrentModificationException. GunakanIterator.remove(),entrySet().removeIf(...), ataumap.entrySet().removeIf(e -> kondisi).Menganggap HashMap thread-safe. Akses paralel tanpa sinkronisasi bisa menyebabkan infinite loop atau data corrupt.
Null key pada implementasi yang melarangnya.
TreeMap,Hashtable, danConcurrentHashMapakan melempar NPE.Kapasitas awal terlalu kecil. Menyimpan 100.000 entry tanpa set kapasitas awal memicu rehashing berkali-kali.
equals/hashCode tidak konsisten. Dua objek
equalstapihashCode-nya beda membuat map kehilangan data.Memakai
InstanceTypewrapper berat sebagai key. Key sebaiknya tipe sederhana sepertiStringatauLong.
Tips Optimasi Performa
java// Perkirakan jumlah entry: capacity = expectedSize / 0.75 + 1
int expectedSize = 100_000;
int capacity = (int) (expectedSize / 0.75f) + 1;
Map<Long, String> besar = new HashMap<>(capacity);
Load factor default 0.75 sudah seimbang untuk hampir semua kasus. Jangan diubah tanpa alasan kuat.
Gunakan
Map.of()atauCollections.emptyMap()untuk data kosong yang konstan.Untuk key
String, hash-nya di-cache oleh JDK sejak Java 7 sehingga aman dipakai berulang.Jika membutuhkan map dengan tipe key primitif (hemat memori dan tanpa boxing), pertimbangkan library seperti Eclipse Collections atau Fastutil.
Checklist Ringkas
Deklarasikan variabel dengan tipe interface
Map<K, V>, bukan class konkret.Pilih implementasi sesuai kebutuhan urutan, null, dan konkurensi.
Gunakan
computeIfAbsent(),merge(), dangetOrDefault()alih-alih null check manual.Pastikan custom key meng-override
equals()danhashCode()dan bersifat immutable.Untuk multi-thread, gunakan
ConcurrentHashMapdan hanya operasi atomiknya.Set kapasitas awal jika jumlah entry sudah diketahui.
Map adalah salah satu bagian paling fundamental di Java, dan menguasai implementasi beserta method modernnya akan langsung terasa pada kualitas kode: lebih ringkas, lebih cepat, dan lebih sedikit bug produksi.