List

List #

Hampir setiap program Java yang nyata perlu menyimpan kumpulan data — daftar produk, antrian tugas, riwayat transaksi, atau hasil query dari database. Java menyediakan interface List beserta beberapa implementasinya, masing-masing dirancang untuk skenario berbeda. Memilih implementasi yang salah tidak akan membuat program error, tapi bisa membuat performa anjlok drastis saat data tumbuh. Artikel ini membahas lima implementasi List yang paling sering dipakai, cara kerja internalnya, operasi-operasi umum yang tersedia, dan panduan memilih mana yang tepat untuk situasi kamu.

Gambaran Umum List #

List adalah interface di Java Collections Framework yang merepresentasikan urutan elemen berindeks. Berbeda dengan Set, List mengizinkan elemen duplikat dan menjamin urutan penyimpanan — elemen ke-0 selalu di posisi pertama, elemen ke-1 di posisi kedua, dan seterusnya.

flowchart TD
    A["«interface»\nCollection"] --> B["«interface»\nList"]
    B --> C["ArrayList"]
    B --> D["LinkedList"]
    B --> E["Vector"]
    E --> F["Stack"]
    B --> G["CopyOnWriteArrayList"]

Semua implementasi List berbagi operasi dasar yang sama karena semuanya mengimplementasikan interface yang sama. Perbedaannya ada di cara kerja internal dan karakteristik performa — bukan di API yang tersedia.

Implementasi Struktur Internal Thread-Safe Kasus Terbaik
ArrayList Array dinamis Akses acak berindeks
LinkedList Doubly linked list Insert/delete di awal/akhir
Vector Array dinamis Legacy multithreading
Stack Array dinamis (LIFO) Tumpukan data LIFO
CopyOnWriteArrayList Array dengan copy-on-write Banyak baca, jarang tulis
Deklarasikan variabel dengan tipe interface List, bukan tipe konkret: List<String> list = new ArrayList<>() bukan ArrayList<String> list = new ArrayList<>(). Dengan begitu kamu bisa ganti implementasi kapan saja tanpa mengubah kode yang memakainya.

ArrayList #

ArrayList adalah implementasi List yang paling sering dipakai. Di balik layar, ia menyimpan elemen dalam sebuah array biasa. Saat array penuh, Java otomatis membuat array baru yang lebih besar (biasanya 1,5× ukuran sebelumnya) dan menyalin semua elemen ke sana.

Karena berbasis array, akses elemen berdasarkan indeks berlangsung dalam O(1) — sangat cepat. Tapi menyisipkan atau menghapus elemen di tengah membutuhkan pergeseran semua elemen sesudahnya, yang berarti O(n).

flowchart LR
    subgraph "ArrayList internal (capacity=6)"
        direction LR
        A["\"[0"]\nApple"] --- B["\"[1"]\nBanana"] --- C["\"[2"]\nCherry"] --- D["\"[3"]\n_"] --- E["\"[4"]\n_"] --- F["\"[5"]\n_"]
    end

Membuat dan Mengisi #

import java.util.ArrayList;
import java.util.List;

// Membuat ArrayList kosong dengan tipe generik
List<String> buah = new ArrayList<>();

// Menambahkan elemen di akhir — O(1) amortized
buah.add("Apel");
buah.add("Mangga");
buah.add("Jeruk");

// Menambahkan elemen di indeks tertentu — O(n) karena perlu geser elemen lain
buah.add(1, "Pisang"); // hasil: [Apel, Pisang, Mangga, Jeruk]

// Membuat ArrayList langsung dari koleksi lain
List<String> salinan = new ArrayList<>(buah);

// List.of() menghasilkan list yang tidak bisa diubah (immutable)
List<String> tetap = List.of("Satu", "Dua", "Tiga");

Membaca dan Mencari #

// Akses berdasarkan indeks — O(1)
String pertama = buah.get(0);
String terakhir = buah.get(buah.size() - 1);

// Cek keberadaan elemen — O(n)
boolean adaMangga = buah.contains("Mangga"); // true

// Cari indeks elemen — O(n)
int indeks = buah.indexOf("Pisang");         // 1
int terakhirKali = buah.lastIndexOf("Apel"); // 0

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

Mengubah dan Menghapus #

// Ganti elemen di indeks tertentu — O(1)
buah.set(0, "Semangka"); // [Semangka, Pisang, Mangga, Jeruk]

// Hapus berdasarkan indeks — O(n) karena perlu geser
buah.remove(2); // hapus "Mangga" → [Semangka, Pisang, Jeruk]

// Hapus berdasarkan nilai — O(n)
buah.remove("Pisang"); // [Semangka, Jeruk]

// Hapus semua elemen
buah.clear();

// Hapus elemen yang memenuhi kondisi (Java 8+)
List<Integer> angka = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));
angka.removeIf(n -> n % 2 == 0); // hapus semua genap → [1, 3, 5]

Iterasi #

List<String> kota = new ArrayList<>(List.of("Jakarta", "Bandung", "Surabaya", "Medan"));

// For-each — paling umum dan bersih
for (String k : kota) {
    System.out.println(k);
}

// For klasik — jika perlu indeks
for (int i = 0; i < kota.size(); i++) {
    System.out.println(i + ": " + kota.get(i));
}

// forEach dengan lambda (Java 8+)
kota.forEach(k -> System.out.println(k.toUpperCase()));

// ANTI-PATTERN: hapus elemen saat for-each — menyebabkan ConcurrentModificationException
for (String k : kota) {
    if (k.startsWith("B")) {
        kota.remove(k); // ✗ JANGAN: akan throw ConcurrentModificationException
    }
}

// BENAR: gunakan removeIf — aman dan bersih
kota.removeIf(k -> k.startsWith("B")); // ✓

Sorting dan Transformasi #

import java.util.Collections;
import java.util.Comparator;

List<String> nama = new ArrayList<>(List.of("Budi", "Ani", "Citra", "Doni"));

// Urutkan ascending (natural order)
Collections.sort(nama); // [Ani, Budi, Citra, Doni]

// Urutkan descending
nama.sort(Comparator.reverseOrder()); // [Doni, Citra, Budi, Ani]

// Urutkan berdasarkan panjang string
nama.sort(Comparator.comparingInt(String::length));

// Acak, balik, dan sub-list
Collections.shuffle(nama);
Collections.reverse(nama);
List<String> sebagian = nama.subList(1, 3); // elemen indeks 1 dan 2 (view, bukan salinan)

LinkedList #

LinkedList menyimpan elemen dalam node yang saling terhubung. Setiap node menyimpan datanya sendiri plus referensi ke node sebelum dan sesudahnya (doubly linked). Tidak ada array di balik layar — elemen tersebar di memori dan dihubungkan lewat pointer.

Akibatnya, akses berdasarkan indeks lambat (O(n) — harus menelusuri dari awal), tapi menyisipkan atau menghapus elemen di posisi awal atau akhir sangat cepat (O(1)). LinkedList juga mengimplementasikan interface Deque, jadi ia bisa dipakai sebagai queue (antrian) maupun stack.

flowchart LR
    A["null ←\nApel\n→"] <--> B["← Pisang →"] <--> C["← Mangga\n→ null"]

Operasi List Biasa #

import java.util.LinkedList;
import java.util.List;

List<String> antrean = new LinkedList<>();
antrean.add("Tugas A");
antrean.add("Tugas B");
antrean.add("Tugas C");

String elemen = antrean.get(1); // "Tugas B" — tapi ini O(n)!
antrean.remove(0);              // hapus "Tugas A"

Operasi Khusus di Awal dan Akhir #

Ini adalah kekuatan utama LinkedList — semua operasi berikut berjalan dalam O(1).

LinkedList<String> ll = new LinkedList<>();
ll.add("Tengah");

// Tambah di awal dan akhir — O(1)
ll.addFirst("Pertama");
ll.addLast("Terakhir");
// hasil: [Pertama, Tengah, Terakhir]

// Lihat tanpa hapus
String kepala = ll.peekFirst(); // "Pertama"
String ekor   = ll.peekLast();  // "Terakhir"

// Hapus dari awal dan akhir — O(1)
String diambil = ll.removeFirst(); // "Pertama"
ll.removeLast();                   // "Terakhir"

Sebagai Queue #

// Pakai sebagai Queue: tambah di belakang (offer), ambil dari depan (poll)
LinkedList<String> queue = new LinkedList<>();
queue.offer("Antri 1");
queue.offer("Antri 2");
queue.offer("Antri 3");

String dilayani = queue.poll(); // "Antri 1" — FIFO

Anti-Pattern Akses Indeks #

LinkedList<Integer> data = new LinkedList<>();
for (int i = 0; i < 10000; i++) data.add(i);

// ANTI-PATTERN: get(i) di LinkedList = O(n) × n iterasi = O(n²) total
for (int i = 0; i < data.size(); i++) {
    System.out.println(data.get(i)); // ✗ sangat lambat
}

// BENAR: pakai for-each yang menelusuri pointer secara berurutan — O(n)
for (int nilai : data) {
    System.out.println(nilai); // ✓
}

Vector #

Vector adalah implementasi List tertua di Java — ada sejak Java 1.0, sebelum Collections Framework lahir di Java 2. Strukturnya hampir identik dengan ArrayList (array dinamis), tapi setiap metodenya disinkronkan (synchronized), artinya hanya satu thread yang bisa mengaksesnya dalam satu waktu.

Operasi Dasar #

import java.util.Vector;
import java.util.List;

List<String> data = new Vector<>();
data.add("Satu");
data.add("Dua");
data.add("Tiga");

String elemen = data.get(1); // "Dua"
data.set(0, "Nol");
data.remove(2);
System.out.println(data.size()); // 2

Mengapa Tidak Direkomendasikan untuk Kode Baru #

API-nya sama persis dengan ArrayList, tapi sinkronisasi di level metode itu kasar dan mahal — setiap operasi mengunci seluruh struktur bahkan saat tidak dibutuhkan. Vector tetap ada di Java karena alasan kompatibilitas ke belakang.

Untuk kode baru, pakai ArrayList jika single-thread, atau Collections.synchronizedList(new ArrayList<>()) dan CopyOnWriteArrayList jika butuh thread-safety. Vector hampir tidak pernah direkomendasikan lagi.

Stack #

Stack adalah subclass dari Vector yang menambahkan operasi tumpukan (LIFO — Last In, First Out). Elemen yang terakhir dimasukkan adalah yang pertama diambil — seperti tumpukan piring.

flowchart TB
    subgraph Stack
        direction TB
        C["C ← top (push/pop di sini)"]
        B["B"]
        A["A ← bottom"]
    end

Operasi Push, Pop, dan Peek #

import java.util.Stack;

Stack<String> riwayat = new Stack<>();

// push: tambahkan ke atas tumpukan — O(1)
riwayat.push("Halaman 1");
riwayat.push("Halaman 2");
riwayat.push("Halaman 3");

// peek: lihat elemen teratas tanpa menghapus — O(1)
String sekarang = riwayat.peek(); // "Halaman 3"

// pop: ambil dan hapus elemen teratas — O(1)
String kembali = riwayat.pop();   // "Halaman 3"

// Selalu cek isEmpty sebelum pop
if (!riwayat.isEmpty()) {
    riwayat.pop();
}

Contoh Nyata: Validasi Kurung #

Skenario klasik penggunaan stack adalah memeriksa keseimbangan kurung dalam ekspresi matematika.

public static boolean kurungSeimbang(String ekspresi) {
    Stack<Character> tumpukan = new Stack<>();

    for (char c : ekspresi.toCharArray()) {
        if (c == '(' || c == '[' || c == '{') {
            tumpukan.push(c);
        } else if (c == ')' || c == ']' || c == '}') {
            if (tumpukan.isEmpty()) return false;
            char buka = tumpukan.pop();
            if (c == ')' && buka != '(') return false;
            if (c == ']' && buka != '[') return false;
            if (c == '}' && buka != '{') return false;
        }
    }
    return tumpukan.isEmpty();
}

System.out.println(kurungSeimbang("(a + b) * [c - {d}]")); // true
System.out.println(kurungSeimbang("(a + [b)]"));            // false
Untuk implementasi stack di kode baru, Java merekomendasikan Deque (biasanya ArrayDeque) daripada Stack. ArrayDeque lebih cepat karena tidak ter-sinkronisasi, dan API-nya lebih eksplisit: push(), pop(), peek().

CopyOnWriteArrayList #

CopyOnWriteArrayList adalah implementasi thread-safe yang menggunakan strategi berbeda dari Vector. Alih-alih mengunci semua operasi, ia membuat salinan penuh array setiap kali terjadi operasi tulis (tambah, hapus, ubah). Operasi baca tidak pernah diblokir karena mereka selalu membaca snapshot array yang ada.

sequenceDiagram
    participant T1 as Thread 1 (baca)
    participant T2 as Thread 2 (tulis)
    participant Arr as Array [A, B, C]

    T1->>Arr: baca → dapat [A, B, C] (tidak diblokir)
    T2->>Arr: tambah "D" → buat salinan [A, B, C, D]
    T2->>Arr: ganti referensi ke salinan baru
    T1->>Arr: baca lagi → dapat [A, B, C, D]

Operasi Dasar #

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

List<String> listener = new CopyOnWriteArrayList<>();
listener.add("Listener A");
listener.add("Listener B");
listener.add("Listener C");

// Iterasi aman — tidak akan throw ConcurrentModificationException
// meskipun thread lain sedang memodifikasi list di waktu bersamaan
for (String l : listener) {
    System.out.println("Notifikasi ke: " + l);
}

Contoh Nyata: Event Listener System #

CopyOnWriteArrayList<Runnable> eventHandlers = new CopyOnWriteArrayList<>();

eventHandlers.add(() -> System.out.println("Handler 1 dipanggil"));
eventHandlers.add(() -> System.out.println("Handler 2 dipanggil"));

// Jalankan semua handler — aman diiterasi meski ada penambahan bersamaan
for (Runnable handler : eventHandlers) {
    handler.run();
}

Kapan Tidak Menggunakannya #

// ANTI-PATTERN: pakai CopyOnWriteArrayList untuk data yang sering berubah
CopyOnWriteArrayList<Integer> seringBerubah = new CopyOnWriteArrayList<>();

// ✗ JANGAN: setiap add() membuat salinan array penuh — sangat mahal
for (int i = 0; i < 10000; i++) {
    seringBerubah.add(i); // 10.000 salinan array dibuat!
}

// BENAR: gunakan hanya jika baca >> tulis
// Untuk kasus tulis sering di multithreading, pakai Collections.synchronizedList()

Operasi Umum dengan Collections #

Class utilitas Collections menyediakan banyak operasi yang bekerja pada semua implementasi List.

Pencarian dan Statistik #

import java.util.Collections;

List<Integer> nilai = new ArrayList<>(List.of(3, 1, 4, 1, 5, 9, 2, 6, 5, 3));

int maks = Collections.max(nilai);              // 9
int min  = Collections.min(nilai);              // 1
int freq = Collections.frequency(nilai, 5);     // 2 (angka 5 muncul 2 kali)

// Binary search — list HARUS sudah terurut dulu
Collections.sort(nilai);
int indeks = Collections.binarySearch(nilai, 6); // indeks elemen 6

Transformasi dan Proteksi #

// Membuat list yang tidak bisa diubah
List<String> tetap = Collections.unmodifiableList(new ArrayList<>(List.of("A", "B")));
// tetap.add("C"); // throw UnsupportedOperationException

// Membuat list thread-safe dari ArrayList biasa
List<String> amanThread = Collections.synchronizedList(new ArrayList<>());

// Isi seluruh list dengan satu nilai
List<String> diisi = new ArrayList<>(Collections.nCopies(5, "default"));
// hasil: [default, default, default, default, default]

// Tukar posisi dua elemen
Collections.swap(nilai, 0, nilai.size() - 1);

// Isi ulang dengan nilai baru
Collections.fill(nilai, 0); // semua elemen jadi 0

Perbandingan Performa #

Memahami kompleksitas operasi membantu kamu membuat keputusan yang tepat saat data bertumbuh besar.

Operasi ArrayList LinkedList
get(i) — akses indeks O(1) ✓ O(n) ✗
add(e) — tambah di akhir O(1) amortized O(1)
add(i, e) — sisip di tengah O(n) O(n)*
remove(i) — hapus di tengah O(n) O(n)*
addFirst / removeLast O(n) O(1) ✓
Penggunaan memori Lebih hemat Lebih boros (overhead pointer)

*LinkedList perlu O(n) untuk menemukan posisi, lalu O(1) untuk operasi pointer-nya.

Decision Tree Memilih Implementasi #

flowchart TD
    A{"Butuh akses elemen\nacak berdasarkan indeks?"} -- Ya --> B[ArrayList]
    A -- Tidak --> C{"Sering insert/delete\ndi awal atau akhir?"}
    C -- Ya --> D{"Perlu sebagai\nQueue atau Deque?"}
    D -- Ya --> E[LinkedList sebagai Deque]
    D -- Tidak --> F[LinkedList]
    C -- Tidak --> G{"Butuh\nthread-safe?"}
    G -- Tidak --> B
    G -- Ya --> H{"Baca jauh lebih\nsering dari tulis?"}
    H -- Ya --> I[CopyOnWriteArrayList]
    H -- Seimbang --> J["Collections.synchronizedList\natau Vector"]

Kapan Menggunakan Tiap Implementasi #

Gunakan ARRAYLIST jika:
  ✓ Ini pilihan default — pakai ini kalau tidak ada alasan khusus lain
  ✓ Sering akses elemen berdasarkan indeks (get, set)
  ✓ Penambahan elemen lebih sering di akhir daripada di tengah
  ✓ Tidak butuh thread-safety

Gunakan LINKEDLIST jika:
  ✓ Sering menyisipkan atau menghapus elemen di posisi awal
  ✓ Butuh struktur Queue atau Deque (FIFO atau double-ended)
  ✗ Hindari jika sering akses berdasarkan indeks acak

Gunakan VECTOR jika:
  ✗ Hampir tidak ada alasan untuk kode baru
  ✓ Harus berinteraksi dengan kode legacy yang mengharapkan Vector

Gunakan STACK jika:
  ✓ Butuh struktur LIFO yang eksplisit
  ✓ Algoritma DFS, undo/redo, parsing ekspresi
  ✗ Untuk kode baru, pertimbangkan ArrayDeque sebagai pengganti

Gunakan COPYONWRITEARRAYLIST jika:
  ✓ Banyak thread membaca, sedikit yang menulis
  ✓ Sistem event listener atau observer pattern di lingkungan concurrent
  ✗ Hindari jika operasi tulis sering — salinan array penuh sangat mahal

Ringkasan #

  • ArrayList adalah pilihan default — berbasis array dinamis, O(1) untuk akses indeks, cocok untuk hampir semua skenario umum yang tidak butuh thread-safety.
  • LinkedList unggul di operasi awal/akhir — O(1) untuk addFirst, removeFirst, addLast, removeLast. Ia juga bisa dipakai sebagai Queue dan Deque. Hindari akses berdasarkan indeks karena O(n).
  • Vector adalah warisan masa lalu — fungsionalnya sama dengan ArrayList tapi dengan sinkronisasi kasar. Tidak direkomendasikan untuk kode baru.
  • Stack mengikuti prinsip LIFO — operasi push, pop, peek. Berguna untuk algoritma DFS, undo/redo, dan parsing. Untuk kode baru, ArrayDeque adalah pengganti yang lebih cepat.
  • CopyOnWriteArrayList untuk concurrent read-heavy — setiap operasi tulis membuat salinan array baru sehingga pembacaan tidak pernah diblokir. Ideal untuk sistem event listener, tapi mahal jika tulis sering.
  • Deklarasikan dengan tipe List — tulis List<String> list = new ArrayList<>(), bukan ArrayList<String> list = new ArrayList<>(). Fleksibilitas untuk ganti implementasi tanpa mengubah kode lain.
  • Gunakan removeIf bukan hapus saat iterasi — menghapus elemen saat for-each akan melempar ConcurrentModificationException. Pakai removeIf() atau Iterator.remove().
  • Collections menyediakan utilitas lengkapsort, shuffle, reverse, binarySearch, frequency, unmodifiableList, dan banyak lagi — semuanya bekerja di semua implementasi List.

← Sebelumnya: Eksepsi   Berikutnya: Map →

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