Map

Map #

Kamu punya daftar produk dan harganya. Kamu ingin mencari harga berdasarkan nama produk. Dengan List, kamu harus menelusuri semua elemen satu per satu sampai ketemu — O(n). Dengan Map, kamu tinggal memberi kunci dan langsung dapat nilainya — O(1). Inilah kekuatan Map: struktur data yang menyimpan pasangan kunci-nilai (key-value) dan memungkinkan pencarian berdasarkan kunci secara langsung tanpa perlu iterasi. Artikel ini membahas lima implementasi Map di Java, operasi-operasi yang tersedia, cara kerja internal masing-masing, dan panduan memilih yang tepat untuk situasimu.

Gambaran Umum Map #

Map adalah interface di Java Collections Framework. Berbeda dengan List yang mengakses elemen lewat indeks angka, Map mengakses nilai lewat kunci yang kamu tentukan sendiri — bisa String, Integer, atau objek apa pun yang mengimplementasikan equals() dan hashCode().

Tiga aturan dasar Map yang perlu selalu diingat:

AturanKeterangan
Kunci unikSetiap kunci hanya boleh ada satu. put("A", 2) di atas put("A", 1) akan menimpa nilai lama.
Nilai boleh duplikatDua kunci berbeda boleh memiliki nilai yang sama.
Satu kunci, satu nilaiSetiap kunci dipetakan ke tepat satu nilai.
flowchart TD
    A["«interface»\nMap‹K,V›"] --> B["HashMap"]
    A --> C["LinkedHashMap"]
    A --> D["TreeMap"]
    A --> E["Hashtable"]
    A --> F["ConcurrentHashMap"]
    B --> C
ImplementasiUrutanThread-SafeIzinkan null keyKompleksitas
HashMapTidak terjamin✓ (satu)O(1)
LinkedHashMapUrutan insert✓ (satu)O(1)
TreeMapUrutan kunciO(log n)
HashtableTidak terjaminO(1)
ConcurrentHashMapTidak terjaminO(1)
Seperti List, deklarasikan variabel dengan tipe interface: Map<String, Integer> map = new HashMap<>(), bukan HashMap<String, Integer> map = new HashMap<>(). Ini memudahkan mengganti implementasi di kemudian hari tanpa mengubah kode yang memakainya.

HashMap #

HashMap adalah implementasi Map yang paling sering dipakai. Di balik layar, ia menggunakan tabel hash — setiap kunci dikonversi menjadi angka hash yang menentukan di slot mana nilai disimpan. Hasilnya: operasi get dan put berjalan rata-rata dalam O(1), tidak peduli seberapa besar map-nya.

Konsekuensinya adalah urutan. Karena penempatan elemen ditentukan oleh hash, bukan urutan penyisipan, kamu tidak bisa mengandalkan urutan tertentu saat mengiterasi HashMap.

flowchart LR
    subgraph "Tabel Hash"
        direction TB
        S0["slot 0"]
        S1["slot 1: 'B'→2"]
        S2["slot 2"]
        S3["slot 3: 'A'→1"]
        S4["slot 4: 'C'→3"]
    end
    K1["'A'"] -->|"hash('A')=3"| S3
    K2["'B'"] -->|"hash('B')=1"| S1
    K3["'C'"] -->|"hash('C')=4"| S4

Membuat dan Mengisi #

import java.util.HashMap;
import java.util.Map;

Map<String, Integer> stok = new HashMap<>();

// put: tambah atau timpa pasangan kunci-nilai — O(1)
stok.put("Apel", 100);
stok.put("Mangga", 50);
stok.put("Jeruk", 75);

// put dengan kunci yang sudah ada: nilai lama ditimpa, nilai lama dikembalikan
Integer stokLama = stok.put("Apel", 120); // stokLama = 100, stok["Apel"] sekarang 120

// putIfAbsent: hanya isi jika kunci belum ada
stok.putIfAbsent("Pisang", 30);  // berhasil: "Pisang" belum ada
stok.putIfAbsent("Apel", 999);   // diabaikan: "Apel" sudah ada, nilainya tetap 120

// Membuat HashMap sekaligus dari Map lain
Map<String, Integer> salinan = new HashMap<>(stok);

Membaca dan Mencari #

// get: ambil nilai berdasarkan kunci — O(1)
Integer jumlahApel = stok.get("Apel");    // 120
Integer tidakAda   = stok.get("Durian");  // null (kunci tidak ditemukan)

// ANTI-PATTERN: langsung unbox hasil get tanpa cek null
int jumlah = stok.get("Durian"); // ✗ NullPointerException!

// BENAR: gunakan getOrDefault untuk nilai fallback
int jumlahAman = stok.getOrDefault("Durian", 0); // ✓ 0 jika tidak ada

// Cek keberadaan kunci dan nilai — O(1)
boolean adaApel   = stok.containsKey("Apel");    // true
boolean adaNilai50 = stok.containsValue(50);     // true

// Ukuran dan cek kosong
int total = stok.size();     // 4
boolean kosong = stok.isEmpty(); // false

Mengubah dan Menghapus #

// replace: timpa nilai hanya jika kunci sudah ada
stok.replace("Apel", 200);         // berhasil: kunci ada
stok.replace("Durian", 10);        // diabaikan: kunci tidak ada

// replace dengan verifikasi nilai lama (conditional replace)
stok.replace("Apel", 200, 250);    // berhasil hanya jika nilai sekarang 200

// compute: hitung nilai baru berdasarkan nilai lama
stok.compute("Apel", (k, v) -> v == null ? 1 : v + 10); // tambah 10 ke stok Apel

// merge: gabungkan nilai dengan logika kustom
stok.merge("Mangga", 20, Integer::sum); // stok Mangga += 20

// remove berdasarkan kunci — O(1)
stok.remove("Jeruk");

// remove kondisional: hapus hanya jika nilai cocok
stok.remove("Mangga", 70); // hapus hanya jika stok["Mangga"] == 70

// Hapus semua
stok.clear();

Iterasi #

Map<String, Integer> nilai = new HashMap<>();
nilai.put("Matematika", 85);
nilai.put("Fisika", 90);
nilai.put("Kimia", 78);

// Iterasi entry (kunci dan nilai sekaligus) — paling umum
for (Map.Entry<String, Integer> entry : nilai.entrySet()) {
    System.out.println(entry.getKey() + ": " + entry.getValue());
}

// Iterasi kunci saja
for (String mapel : nilai.keySet()) {
    System.out.println(mapel);
}

// Iterasi nilai saja
for (int n : nilai.values()) {
    System.out.println(n);
}

// forEach dengan lambda (Java 8+) — paling ringkas
nilai.forEach((k, v) -> System.out.println(k + " → " + v));

LinkedHashMap #

LinkedHashMap adalah subclass dari HashMap yang menambahkan satu jaminan ekstra: urutan penyisipan selalu dipertahankan. Di balik layar, setiap node di tabel hash juga dihubungkan dengan doubly linked list yang merekam urutan elemen masuk. Ini membuatnya sedikit lebih lambat dan boros memori dibanding HashMap, tapi sangat berguna saat urutan penting.

flowchart LR
    subgraph "Tabel Hash + Linked List"
        direction LR
        A["'A'→1"] -->|"insert order"| B["'B'→2"] -->|"insert order"| C["'C'→3"]
    end

Urutan Penyisipan #

import java.util.LinkedHashMap;
import java.util.Map;

Map<String, Integer> langkah = new LinkedHashMap<>();
langkah.put("Pertama", 1);
langkah.put("Kedua", 2);
langkah.put("Ketiga", 3);
langkah.put("Keempat", 4);

// Iterasi SELALU dalam urutan penyisipan — tidak seperti HashMap
langkah.forEach((k, v) -> System.out.println(v + ". " + k));
// Output (selalu berurutan):
// 1. Pertama
// 2. Kedua
// 3. Ketiga
// 4. Keempat

// Bandingkan dengan HashMap — urutan tidak terjamin:
Map<String, Integer> acak = new HashMap<>(langkah);
acak.forEach((k, v) -> System.out.println(v + ". " + k));
// Output bisa dalam urutan apa saja

Mode Akses (LRU Cache) #

LinkedHashMap punya konstruktor khusus yang mengubah mode pengurutan dari “urutan insert” menjadi “urutan akses” (access-order). Elemen yang paling jarang diakses akan berada di depan. Ini adalah fondasi untuk membangun LRU Cache (Least Recently Used) — cache yang otomatis membuang elemen terlama saat kapasitas penuh.

// accessOrder = true: urutan berdasarkan akses terakhir, bukan insert
int kapasitas = 3;
Map<String, String> lruCache = new LinkedHashMap<>(kapasitas, 0.75f, true) {
    @Override
    protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
        return size() > kapasitas; // otomatis hapus elemen tertua saat melebihi kapasitas
    }
};

lruCache.put("A", "Data A");
lruCache.put("B", "Data B");
lruCache.put("C", "Data C");
// cache: [A, B, C]

lruCache.get("A"); // akses A — A pindah ke belakang (paling baru diakses)
// urutan internal: [B, C, A]

lruCache.put("D", "Data D"); // cache penuh, B (paling jarang diakses) dihapus
// cache: [C, A, D]

System.out.println(lruCache.containsKey("B")); // false — sudah dibuang
System.out.println(lruCache.containsKey("A")); // true — masih ada

TreeMap #

TreeMap menggunakan Red-Black Tree — pohon biner yang seimbang secara otomatis. Setiap kunci disimpan dalam urutan terurut (ascending by default), dan semua operasi berjalan dalam O(log n). Ini lebih lambat dari HashMap, tapi TreeMap punya kemampuan yang tidak dimiliki implementasi lain: navigasi berdasarkan rentang kunci.

flowchart TD
    subgraph "Red-Black Tree"
        B["'B'→2"]
        A["'A'→1"] 
        C["'C'→3"]
        B --> A
        B --> C
    end

Urutan Kunci Otomatis #

import java.util.TreeMap;
import java.util.Map;

Map<String, Integer> peringkat = new TreeMap<>();

// Tambah dalam urutan acak
peringkat.put("Budi", 85);
peringkat.put("Ani", 92);
peringkat.put("Citra", 78);
peringkat.put("Doni", 88);

// Iterasi SELALU dalam urutan kunci (alphabetical untuk String)
peringkat.forEach((nama, skor) -> System.out.println(nama + ": " + skor));
// Output (selalu alphabetical):
// Ani: 92
// Budi: 85
// Citra: 78
// Doni: 88

// ANTI-PATTERN: masukkan null sebagai kunci
// peringkat.put(null, 100); // ✗ NullPointerException — TreeMap tidak terima null key

Urutan Kustom dengan Comparator #

Jika urutan natural tidak sesuai, kamu bisa menyuplai Comparator sendiri ke konstruktor TreeMap.

import java.util.Comparator;

// Urutkan berdasarkan panjang string, lalu alphabetical jika sama panjang
Map<String, Integer> kustom = new TreeMap<>(
    Comparator.comparingInt(String::length).thenComparing(Comparator.naturalOrder())
);

kustom.put("Banana", 3);
kustom.put("Apel", 1);
kustom.put("Mangga", 2);
kustom.put("Kiwi", 4);

kustom.forEach((k, v) -> System.out.println(k + ": " + v));
// Output (urut dari nama terpendek):
// Kiwi: 4
// Apel: 1
// Banana: 3
// Mangga: 2

Ini adalah fitur eksklusif TreeMap yang tidak ada di implementasi lain. Kamu bisa mengambil sub-map, kunci tertinggi/terendah, atau kunci terdekat dari suatu nilai.

import java.util.TreeMap;

TreeMap<Integer, String> jadwal = new TreeMap<>();
jadwal.put(8, "Sarapan");
jadwal.put(12, "Makan Siang");
jadwal.put(15, "Snack");
jadwal.put(19, "Makan Malam");
jadwal.put(22, "Tidur");

// Kunci pertama dan terakhir
System.out.println(jadwal.firstKey()); // 8
System.out.println(jadwal.lastKey());  // 22

// Kunci terdekat di bawah dan di atas suatu nilai
System.out.println(jadwal.floorKey(14));   // 12 (≤ 14)
System.out.println(jadwal.ceilingKey(14)); // 15 (≥ 14)
System.out.println(jadwal.lowerKey(15));   // 12 (< 15, eksklusif)
System.out.println(jadwal.higherKey(15));  // 19 (> 15, eksklusif)

// Sub-map berdasarkan rentang kunci
Map<Integer, String> siangMalam = jadwal.subMap(12, true, 19, true);
// {12=Makan Siang, 15=Snack, 19=Makan Malam}

Map<Integer, String> pagian = jadwal.headMap(12, false); // < 12
// {8=Sarapan}

Map<Integer, String> malam = jadwal.tailMap(19, true); // ≥ 19
// {19=Makan Malam, 22=Tidur}

Hashtable #

Hashtable adalah implementasi Map pertama di Java — hadir sejak Java 1.0, bahkan sebelum Collections Framework lahir. Strukturnya mirip HashMap: tabel hash dengan sinkronisasi. Perbedaannya: semua metodenya synchronized, dan tidak ada satu pun yang menerima null sebagai kunci maupun nilai.

Operasi Dasar #

import java.util.Hashtable;
import java.util.Map;

Map<String, Integer> tabel = new Hashtable<>();
tabel.put("Satu", 1);
tabel.put("Dua", 2);
tabel.put("Tiga", 3);

int nilai = tabel.get("Satu"); // 1
tabel.remove("Dua");
System.out.println(tabel.size()); // 2

// ANTI-PATTERN: masukkan null sebagai kunci atau nilai
// tabel.put(null, 1);    // ✗ NullPointerException
// tabel.put("A", null);  // ✗ NullPointerException

Mengapa Tidak Direkomendasikan untuk Kode Baru #

Hashtable dan Vector punya masalah yang sama: sinkronisasi kasar di level metode. Setiap operasi mengunci seluruh tabel, bahkan saat hanya satu thread yang aktif. Ini menjadi bottleneck serius di aplikasi dengan banyak thread.

Untuk kode baru, pakai HashMap jika single-thread, atau ConcurrentHashMap jika butuh thread-safety. Hashtable tetap ada di Java karena kompatibilitas ke belakang — bukan karena ia pilihan yang baik.

ConcurrentHashMap #

ConcurrentHashMap adalah jawaban modern untuk kebutuhan Map yang thread-safe. Berbeda dari Hashtable yang mengunci seluruh tabel, ConcurrentHashMap menggunakan teknik segmentasi (atau node-level locking di Java 8+) — hanya bagian kecil dari tabel yang dikunci saat operasi tulis, sehingga thread lain tetap bisa membaca dan menulis di bagian lain secara bersamaan.

flowchart LR
    subgraph "ConcurrentHashMap — Node-Level Locking"
        direction LR
        N1["node A\n🔒 Thread 1 sedang tulis"]
        N2["node B\n✓ Thread 2 bisa akses"]
        N3["node C\n✓ Thread 3 bisa akses"]
    end

Operasi Dasar #

import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;

Map<String, Integer> counter = new ConcurrentHashMap<>();
counter.put("halaman-utama", 0);
counter.put("tentang-kami", 0);
counter.put("kontak", 0);

// Operasi baca/tulis aman dari thread mana pun tanpa lock eksplisit
counter.put("halaman-utama", 150);
int kunjungan = counter.get("halaman-utama"); // 150

// ANTI-PATTERN: masukkan null sebagai kunci atau nilai
// counter.put(null, 1);    // ✗ NullPointerException
// counter.put("A", null);  // ✗ NullPointerException

Operasi Atomik #

ConcurrentHashMap menyediakan beberapa metode yang berjalan secara atomik — artinya tidak bisa diganggu thread lain di tengah eksekusi, tanpa kamu perlu tambahan locking manual.

// putIfAbsent atomik: tambah hanya jika kunci belum ada
counter.putIfAbsent("daftar", 0); // aman dari race condition

// compute atomik: update nilai berdasarkan nilai sebelumnya
// Skenario: menghitung kunjungan halaman dari banyak thread sekaligus
counter.compute("halaman-utama", (k, v) -> v == null ? 1 : v + 1);

// merge atomik: gabungkan nilai lama dan baru
counter.merge("halaman-utama", 1, Integer::sum); // tambah 1 ke nilai yang ada

// computeIfAbsent: hitung dan isi nilai hanya jika kunci belum ada
counter.computeIfAbsent("halaman-baru", k -> hitungNilaiAwal(k));

// computeIfPresent: update hanya jika kunci sudah ada
counter.computeIfPresent("halaman-utama", (k, v) -> v + 100);

Contoh Nyata: Menghitung Frekuensi dari Banyak Thread #

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

ConcurrentHashMap<String, Integer> frekuensi = new ConcurrentHashMap<>();
String[] kata = {"java", "map", "java", "list", "map", "java"};

ExecutorService pool = Executors.newFixedThreadPool(3);

for (String k : kata) {
    pool.submit(() -> {
        // merge adalah atomik: aman dijalankan dari banyak thread sekaligus
        frekuensi.merge(k, 1, Integer::sum);
    });
}

pool.shutdown();
pool.awaitTermination(5, TimeUnit.SECONDS);

frekuensi.forEach((k, v) -> System.out.println(k + ": " + v));
// Output (tidak berurutan, tapi nilainya akurat):
// java: 3
// map: 2
// list: 1

Operasi Umum Lintas Implementasi #

Semua implementasi Map berbagi operasi dasar yang sama karena mengimplementasikan interface yang sama. Berikut pola-pola yang sering dipakai di kode nyata.

Nilai Default dan Komputasi Malas #

Map<String, List<String>> grup = new HashMap<>();

// ANTI-PATTERN: cek manual sebelum isi
if (!grup.containsKey("Admin")) {
    grup.put("Admin", new ArrayList<>());
}
grup.get("Admin").add("Budi"); // bertele-tele

// BENAR: computeIfAbsent — buat nilai baru hanya jika belum ada
grup.computeIfAbsent("Admin", k -> new ArrayList<>()).add("Budi");
grup.computeIfAbsent("Admin", k -> new ArrayList<>()).add("Ani"); // list yang sama
grup.computeIfAbsent("User", k -> new ArrayList<>()).add("Citra");

System.out.println(grup.get("Admin")); // [Budi, Ani]
System.out.println(grup.get("User"));  // [Citra]

Mengumpulkan Data ke Map dengan Stream #

import java.util.stream.Collectors;
import java.util.List;

record Mahasiswa(String nama, String jurusan, double ipk) {}

List<Mahasiswa> mahasiswa = List.of(
    new Mahasiswa("Budi", "Informatika", 3.8),
    new Mahasiswa("Ani", "Informatika", 3.5),
    new Mahasiswa("Citra", "Matematika", 3.9),
    new Mahasiswa("Doni", "Matematika", 3.2)
);

// Buat Map nama → ipk
Map<String, Double> ipkMap = mahasiswa.stream()
    .collect(Collectors.toMap(Mahasiswa::nama, Mahasiswa::ipk));

// Kelompokkan berdasarkan jurusan
Map<String, List<Mahasiswa>> perJurusan = mahasiswa.stream()
    .collect(Collectors.groupingBy(Mahasiswa::jurusan));

// Hitung rata-rata ipk per jurusan
Map<String, Double> rataIpk = mahasiswa.stream()
    .collect(Collectors.groupingBy(
        Mahasiswa::jurusan,
        Collectors.averagingDouble(Mahasiswa::ipk)
    ));

rataIpk.forEach((jurusan, rata) ->
    System.out.printf("%s: %.2f%n", jurusan, rata)
);

Mengonversi Map ke List dan Sebaliknya #

Map<String, Integer> harga = new HashMap<>();
harga.put("Apel", 5000);
harga.put("Mangga", 8000);
harga.put("Jeruk", 4000);

// Map → List of keys
List<String> namaItem = new ArrayList<>(harga.keySet());

// Map → List of values
List<Integer> daftarHarga = new ArrayList<>(harga.values());

// Map → List of entries, lalu diurutkan berdasarkan nilai
List<Map.Entry<String, Integer>> entries = new ArrayList<>(harga.entrySet());
entries.sort(Map.Entry.comparingByValue());
entries.forEach(e -> System.out.println(e.getKey() + ": Rp" + e.getValue()));
// Output (urut dari termurah):
// Jeruk: Rp4000
// Apel: Rp5000
// Mangga: Rp8000

Perbandingan Performa #

OperasiHashMapLinkedHashMapTreeMap
get(key)O(1)O(1)O(log n)
put(key, val)O(1)O(1)O(log n)
remove(key)O(1)O(1)O(log n)
containsKey(key)O(1)O(1)O(log n)
IterasiO(n)O(n)O(n)
Rentang kunci (subMap)✗ Tidak ada✗ Tidak ada✓ O(log n)
Overhead memoriRendahSedang (linked list)Sedang (pointer tree)

Decision Tree Memilih Implementasi #

flowchart TD
    A{Butuh\nthread-safe?} -- Ya --> B{Banyak operasi\nbaca bersamaan?}
    B -- Ya --> C[ConcurrentHashMap]
    B -- Tidak --> D["Hashtable\n(tidak disarankan untuk kode baru)"]
    A -- Tidak --> E{Butuh\nurutan kunci?}
    E -- "Urutan insert" --> F[LinkedHashMap]
    E -- "Urutan kunci (sorted)" --> G[TreeMap]
    E -- "Tidak perlu urutan" --> H[HashMap]
    G --> I{Perlu navigasi\nrentang kunci?}
    I -- Ya --> J["TreeMap dengan\nfloorKey/ceilingKey/subMap"]
    I -- Tidak --> G

Kapan Menggunakan Tiap Implementasi #

Gunakan HASHMAP jika:
  ✓ Ini pilihan default — pakai jika tidak ada kebutuhan khusus
  ✓ Performa O(1) untuk get/put/remove adalah prioritas
  ✓ Urutan elemen tidak penting
  ✓ Tidak butuh thread-safety

Gunakan LINKEDHASHMAP jika:
  ✓ Butuh urutan penyisipan terjaga (misal: langkah-langkah proses, form fields)
  ✓ Membangun LRU cache dengan access-order mode
  ✓ Output harus konsisten dan dapat diprediksi urutannya

Gunakan TREEMAP jika:
  ✓ Kunci harus selalu terurut (alphabetical, numerik, kustom)
  ✓ Perlu navigasi berdasarkan rentang kunci (floorKey, ceilingKey, subMap)
  ✓ Membangun struktur seperti kalender, jadwal, atau price range

Gunakan HASHTABLE jika:
  ✗ Hampir tidak ada alasan untuk kode baru
  ✓ Hanya jika harus berinteraksi dengan API legacy yang mengharuskannya

Gunakan CONCURRENTHASHMAP jika:
  ✓ Banyak thread membaca dan menulis secara bersamaan
  ✓ Perlu operasi atomik seperti compute, merge, putIfAbsent tanpa lock manual
  ✗ Hindari jika hanya single-thread — overhead-nya tidak sebanding

Ringkasan #

  • Map menyimpan pasangan kunci-nilai — kunci harus unik, nilai boleh duplikat. Pencarian berdasarkan kunci berjalan dalam O(1) untuk implementasi berbasis hash.
  • HashMap adalah pilihan default — tidak terurut, O(1) untuk semua operasi dasar, mengizinkan satu null key. Pilih ini kalau tidak ada kebutuhan khusus lain.
  • LinkedHashMap menjamin urutan insert — subclass HashMap yang menambahkan doubly linked list internal. Juga bisa dikonfigurasi ke access-order untuk membangun LRU cache.
  • TreeMap selalu terurut berdasarkan kunci — berbasis Red-Black Tree, O(log n) untuk semua operasi. Punya API navigasi eksklusif: floorKey, ceilingKey, subMap, headMap, tailMap.
  • Hashtable adalah warisan masa lalu — thread-safe tapi kasar, tidak terima null. Gunakan ConcurrentHashMap sebagai penggantinya di kode baru.
  • ConcurrentHashMap untuk multithreading modern — locking di level node, bukan seluruh tabel. Menyediakan operasi atomik compute, merge, putIfAbsent yang aman dari race condition tanpa lock manual.
  • Gunakan getOrDefault bukan get mentahget mengembalikan null jika kunci tidak ada. Unboxing null ke int langsung menyebabkan NullPointerException.
  • computeIfAbsent untuk nilai yang perlu diinisialisasi — pola if (!map.containsKey(k)) map.put(k, new ArrayList<>()) bisa diganti satu baris: map.computeIfAbsent(k, x -> new ArrayList<>()).

← Sebelumnya: List   Berikutnya: Date & Time →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact