Java Collections Framework (Struktur Data di Memori)

6 min readFundamental
JavaCollectionsData StructuresBackend

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.

AntarmukaKarakteristikContoh Implementasi
ListBerurutan, boleh duplikatArrayList, LinkedList
SetUnik, tanpa duplikatHashSet, TreeSet
MapPasangan key-valueHashMap, 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):

OperasiKompleksitas
get(index)O(1)
add(...) di akhirO(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:

OperasiKompleksitas
addFirst / addLast / removeFirst / removeLastO(1)
get(index)O(n) — harus berjalan menelusuri node
remove(obj) di tengahO(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 dominannya get(index) atau contains() — 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

AspekHashSetTreeSet
UrutanAcak (berdasarkan hash)Terurut naik (natural order / Comparator)
add / containsO(1) rata-rataO(log n)
Struktur dasarHash tableRed-black tree
NullBoleh (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

AspekHashMapTreeMap
Urutan keyAcak (hash)Terurut (red-black tree)
put / getO(1) rata-rataO(log n)
Key nullBoleh (satu)Tidak boleh
Fitur unggulanKecepatanRange 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

KebutuhanPilih
Akses cepat by index, data jarang disisip di tengahArrayList
Antrian/kebalikan dengan sisip-hapus di ujungLinkedList
Cek keanggotaan super cepat tanpa duplikatHashSet
Elemen unik yang harus selalu terurutTreeSet
Key-value lookup cepat (JSON, cache)HashMap
Key-value yang perlu iterasi terurut / range queryTreeMap

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.

← Back to technical articles