Java Collections Framework (Struktur Data di Memori)
Sebelum data disimpan ke database, semua data hidup dulu di dalam memori RAM. Cara Anda menyimpan, mencari, dan memanipulasi data di memori sangat menentukan performa aplikasi. Di sinilah peran Java Collections Framework — kumpulan antarmuka dan implementasi struktur data yang sudah siap pakai.
Array vs Collection: Statis vs Dinamis
Array di Java berukuran tetap: begitu dibuat dengan new String[5], ukurannya tidak bisa diubah lagi.
String[] nama = new String[5]; // ukuran kaku: 5, tidak bisa lebih
nama[0] = "Hendro";
Collection bersifat dinamis: bisa bertambah dan berkurang sesuai kebutuhan, tanpa perlu menebak ukuran di awal. Untuk backend yang menerima data dari pengguna (jumlahnya tidak pernah kita tahu), Collection adalah pilihan yang hampir selalu benar.
List<String> nama = new ArrayList<>(); // bebas bertambah
nama.add("Hendro");
nama.add("Endos");
nama.remove("Endos");
Keseluruhan framework dibangun di atas tiga antarmuka utama ("The Big Three"): List, Set, dan Map.
| Antarmuka | Karakteristik | Contoh Implementasi |
|---|---|---|
List | Berurutan, boleh duplikat | ArrayList, LinkedList |
Set | Unik, tanpa duplikat | HashSet, TreeSet |
Map | Pasangan key-value | HashMap, TreeMap |
1. List: Data Berurutan
List adalah antarmuka paling sering dipakai. Dua implementasinya punya karakteristik yang sangat berbeda, dan memilih yang salah adalah sumber masalah performa.
ArrayList — Pencarian cepat berdasarkan indeks
ArrayList menyimpan data dalam array di belakang layar yang tumbuh otomatis. Karena data berdekatan di memori, mengakses elemen ke-n sangat cepat.
List<User> users = new ArrayList<>();
users.add(user1);
users.get(0); // O(1) — akses langsung berdasarkan indeks
Kompleksitas waktu (Big-O):
| Operasi | Kompleksitas |
|---|---|
get(index) | O(1) |
add(...) di akhir | O(1) rata-rata |
add(index, ...) / remove(index) | O(n) — elemen setelahnya harus digeser |
contains(x) | O(n) — harus scan satu per satu |
Gunakan ArrayList saat: akses acak berdasarkan indeks dominan, dan operasi penambahan/penghapusan hanya di ujung list.
LinkedList — Sisip & hapus yang intensif
LinkedList menyimpan data sebagai rangkaian node yang saling menunjuk (satu node tahu node berikutnya). Menyisipkan di tengah cukup "memutus" dan "menyambung" dua penunjuk, tanpa menggeser elemen lain.
List<String> antrian = new LinkedList<>();
antrian.addLast("A"); // O(1) menambah di ujung
antrian.addFirst("B"); // O(1) menambah di awal
String depan = antrian.removeFirst(); // O(1) — enak untuk antrian (queue)
Kompleksitas waktu:
| Operasi | Kompleksitas |
|---|---|
addFirst / addLast / removeFirst / removeLast | O(1) |
get(index) | O(n) — harus berjalan menelusuri node |
remove(obj) di tengah | O(n) — tetap perlu mencari objeknya |
Gunakan LinkedList saat: operasi sisip/hapus di kedua ujung sangat intensif (misalnya antrian/message queue di memori), dan akses acak jarang.
Perangkap umum: banyak orang memilih
LinkedList"agar cepat", padahal operasi dominannyaget(index)ataucontains()— dua-duanya justru lambat di LinkedList. Selalu ukur operasi mana yang paling sering, bukan sekadar nama struktur datanya.
2. Set: Elemen Unik Tanpa Duplikasi
Set menjamin setiap elemen hanya muncul sekali. Ini contoh kasus nyata di backend: menyimpan token sesi aktif atau memvalidasi email unik.
Set<String> emailTerdaftar = new HashSet<>();
emailTerdaftar.add("hendro@mail.com");
emailTerdaftar.add("hendro@mail.com"); // diabaikan — sudah ada
emailTerdaftar.size(); // 1
HashSet vs TreeSet
| Aspek | HashSet | TreeSet |
|---|---|---|
| Urutan | Acak (berdasarkan hash) | Terurut naik (natural order / Comparator) |
add / contains | O(1) rata-rata | O(log n) |
| Struktur dasar | Hash table | Red-black tree |
| Null | Boleh (satu) | Tidak boleh |
// HashSet: cek keanggotaan super cepat — O(1)
if (activeSessionTokens.contains(token)) {
// token valid, proses request
}
// TreeSet: otomatis terurut, misal ranking/urutan angka
TreeSet<Integer> antrianPrioritas = new TreeSet<>();
antrianPrioritas.add(5);
antrianPrioritas.add(1);
antrianPrioritas.first(); // 1 — elemen terkecil, O(log n)
Aturan praktis backend: default ke HashSet untuk cek keanggotaan. Gunakan TreeSet hanya bila Anda benar-benar butuh iterasi dalam urutan terurut.
3. Map: Pasangan Key-Value
Map menyimpan data dalam bentuk pasangan kunci → nilai. Ini adalah struktur data yang paling banyak dipakai di backend, di antaranya untuk memetakan payload JSON, konfigurasi sistem, dan in-memory cache.
// Pemetaan payload JSON yang masuk dari frontend
Map<String, Object> payload = new HashMap<>();
payload.put("email", "hendro@mail.com");
payload.put("role", "ADMIN");
String email = (String) payload.get("email"); // O(1)
HashMap vs TreeMap
| Aspek | HashMap | TreeMap |
|---|---|---|
| Urutan key | Acak (hash) | Terurut (red-black tree) |
put / get | O(1) rata-rata | O(log n) |
Key null | Boleh (satu) | Tidak boleh |
| Fitur unggulan | Kecepatan | Range query (subMap, firstKey, dll.) |
// In-memory cache sederhana — HashMap paling cocok
Map<String, ConfigValue> cache = new HashMap<>();
ConfigValue v = cache.computeIfAbsent(configKey, ConfigService::loadFromDb);
// TreeMap: butuh akses key dalam urutan, misal session timeout yang diurutkan
TreeMap<Long, String> sessionByExpiry = new TreeMap<>();
Long nearestExpiry = sessionByExpiry.firstKey();
Aturan praktis backend: default ke HashMap. Gunakan TreeMap hanya bila perlu iterasi key terurut atau query rentang (range query).
Iterasi & Manipulasi Data
Ada tiga cara umum membaca isi Collection.
1. Enhanced For-Loop (paling sering)
for (User user : users) {
System.out.println(user.getEmail());
}
2. Iterator (aman saat menghapus di tengah iterasi)
Iterator adalah cara fail-safe untuk menghapus elemen sambil iterasi — menghapus langsung dari list dalam enhanced for-loop akan melempar ConcurrentModificationException.
Iterator<String> it = names.iterator();
while (it.hasNext()) {
String name = it.next();
if (name.startsWith("x")) {
it.remove(); // aman — hapus lewat iterator
}
}
3. Java Streams (ekspresif untuk transformasi & filter)
Stream API memungkinkan Anda memfilter, memetakan, dan mengumpulkan data dalam satu rangkaian operasi yang jelas dibaca:
// Tanpa stream: banyak loop bersarang
List<User> aktif = new ArrayList<>();
for (User u : users) {
if (u.isActive() && u.getRole().equals("ADMIN")) {
aktif.add(u);
}
}
// Dengan stream: intent langsung terlihat
List<User> adminAktif = users.stream()
.filter(User::isActive)
.filter(u -> u.getRole().equals("ADMIN"))
.sorted(Comparator.comparing(User::getCreatedAt))
.toList(); // Java 16+; sebelumnya .collect(toList())
Contoh lain yang sering muncul di service layer backend:
Map<String, Long> jumlahPerRole = users.stream()
.collect(Collectors.groupingBy(User::getRole, Collectors.counting()));
List<String> emailAdmin = users.stream()
.filter(u -> u.getRole().equals("ADMIN"))
.map(User::getEmail)
.toList();
Ringkasan Cepat Memilih
| Kebutuhan | Pilih |
|---|---|
| Akses cepat by index, data jarang disisip di tengah | ArrayList |
| Antrian/kebalikan dengan sisip-hapus di ujung | LinkedList |
| Cek keanggotaan super cepat tanpa duplikat | HashSet |
| Elemen unik yang harus selalu terurut | TreeSet |
| Key-value lookup cepat (JSON, cache) | HashMap |
| Key-value yang perlu iterasi terurut / range query | TreeMap |
Kuasai pilihan struktur data ini, dan Anda sudah menghemat performa backend sebelum kode menyentuh database. Lanjut ke Anatomi Protokol HTTP untuk memahami bagaimana klien dan server berkomunikasi.