sch39
Part 4 of 2 — Java Basics

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() dan hashCode() dari key.

  • Urutan elemen tergantung implementasi — HashMap tidak menjamin apa pun.

Implementasi yang Perlu Diketahui

Implementasi

Urutan

Null key

Null value

get/put

Thread-safe

HashMap

Tidak dijamin

1

Ya

O(1) rata-rata

Tidak

LinkedHashMap

Insertion / access order

1

Ya

O(1) rata-rata

Tidak

TreeMap

Terurut (sorted)

Tidak

Ya

O(log n)

Tidak

Hashtable

Tidak dijamin

Tidak

Tidak

O(1) rata-rata

Ya (legacy)

ConcurrentHashMap

Tidak dijamin

Tidak

Tidak

O(1) rata-rata

Ya

Aturan praktis: default ke HashMap. Gunakan LinkedHashMap kalau urutan penting, TreeMap kalau butuh operasi range/sorted, dan ConcurrentHashMap kalau diakses banyak thread. Hashtable sebaiknya 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

  1. Mutasi key setelah dimasukkan. Hash berubah, entry tidak bisa ditemukan lagi. Gunakan key immutable.

  2. Modifikasi map saat iterasi. for (var e : map.entrySet()) map.remove(e.getKey()); melempar ConcurrentModificationException. Gunakan Iterator.remove(), entrySet().removeIf(...), atau map.entrySet().removeIf(e -> kondisi).

  3. Menganggap HashMap thread-safe. Akses paralel tanpa sinkronisasi bisa menyebabkan infinite loop atau data corrupt.

  4. Null key pada implementasi yang melarangnya. TreeMap, Hashtable, dan ConcurrentHashMap akan melempar NPE.

  5. Kapasitas awal terlalu kecil. Menyimpan 100.000 entry tanpa set kapasitas awal memicu rehashing berkali-kali.

  6. equals/hashCode tidak konsisten. Dua objek equals tapi hashCode-nya beda membuat map kehilangan data.

  7. Memakai InstanceType wrapper berat sebagai key. Key sebaiknya tipe sederhana seperti String atau Long.

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() atau Collections.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(), dan getOrDefault() alih-alih null check manual.

  • Pastikan custom key meng-override equals() dan hashCode() dan bersifat immutable.

  • Untuk multi-thread, gunakan ConcurrentHashMap dan 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.