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:
| Aturan | Keterangan |
|---|---|
| Kunci unik | Setiap kunci hanya boleh ada satu. put("A", 2) di atas put("A", 1) akan menimpa nilai lama. |
| Nilai boleh duplikat | Dua kunci berbeda boleh memiliki nilai yang sama. |
| Satu kunci, satu nilai | Setiap 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| Implementasi | Urutan | Thread-Safe | Izinkan null key | Kompleksitas |
|---|---|---|---|---|
HashMap | Tidak terjamin | ✗ | ✓ (satu) | O(1) |
LinkedHashMap | Urutan insert | ✗ | ✓ (satu) | O(1) |
TreeMap | Urutan kunci | ✗ | ✗ | O(log n) |
Hashtable | Tidak terjamin | ✓ | ✗ | O(1) |
ConcurrentHashMap | Tidak terjamin | ✓ | ✗ | O(1) |
SepertiList, deklarasikan variabel dengan tipe interface:Map<String, Integer> map = new HashMap<>(), bukanHashMap<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"| S4Membuat 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"]
endUrutan 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
endUrutan 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
Navigasi Rentang Kunci #
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, pakaiHashMapjika single-thread, atauConcurrentHashMapjika butuh thread-safety.Hashtabletetap 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"]
endOperasi 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 #
| Operasi | HashMap | LinkedHashMap | TreeMap |
|---|---|---|---|
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) |
| Iterasi | O(n) | O(n) | O(n) |
Rentang kunci (subMap) | ✗ Tidak ada | ✗ Tidak ada | ✓ O(log n) |
| Overhead memori | Rendah | Sedang (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 --> GKapan 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 #
Mapmenyimpan pasangan kunci-nilai — kunci harus unik, nilai boleh duplikat. Pencarian berdasarkan kunci berjalan dalam O(1) untuk implementasi berbasis hash.HashMapadalah pilihan default — tidak terurut, O(1) untuk semua operasi dasar, mengizinkan satunullkey. Pilih ini kalau tidak ada kebutuhan khusus lain.LinkedHashMapmenjamin urutan insert — subclassHashMapyang menambahkan doubly linked list internal. Juga bisa dikonfigurasi ke access-order untuk membangun LRU cache.TreeMapselalu terurut berdasarkan kunci — berbasis Red-Black Tree, O(log n) untuk semua operasi. Punya API navigasi eksklusif:floorKey,ceilingKey,subMap,headMap,tailMap.Hashtableadalah warisan masa lalu — thread-safe tapi kasar, tidak terimanull. GunakanConcurrentHashMapsebagai penggantinya di kode baru.ConcurrentHashMapuntuk multithreading modern — locking di level node, bukan seluruh tabel. Menyediakan operasi atomikcompute,merge,putIfAbsentyang aman dari race condition tanpa lock manual.- Gunakan
getOrDefaultbukangetmentah —getmengembalikannulljika kunci tidak ada. Unboxingnullkeintlangsung menyebabkanNullPointerException.computeIfAbsentuntuk nilai yang perlu diinisialisasi — polaif (!map.containsKey(k)) map.put(k, new ArrayList<>())bisa diganti satu baris:map.computeIfAbsent(k, x -> new ArrayList<>()).