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 interfaceList, bukan tipe konkret:List<String> list = new ArrayList<>()bukanArrayList<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, pakaiArrayListjika single-thread, atauCollections.synchronizedList(new ArrayList<>())danCopyOnWriteArrayListjika butuh thread-safety.Vectorhampir 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 merekomendasikanDeque(biasanyaArrayDeque) daripadaStack.ArrayDequelebih 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 #
ArrayListadalah pilihan default — berbasis array dinamis, O(1) untuk akses indeks, cocok untuk hampir semua skenario umum yang tidak butuh thread-safety.LinkedListunggul di operasi awal/akhir — O(1) untukaddFirst,removeFirst,addLast,removeLast. Ia juga bisa dipakai sebagaiQueuedanDeque. Hindari akses berdasarkan indeks karena O(n).Vectoradalah warisan masa lalu — fungsionalnya sama denganArrayListtapi dengan sinkronisasi kasar. Tidak direkomendasikan untuk kode baru.Stackmengikuti prinsip LIFO — operasipush,pop,peek. Berguna untuk algoritma DFS, undo/redo, dan parsing. Untuk kode baru,ArrayDequeadalah pengganti yang lebih cepat.CopyOnWriteArrayListuntuk 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— tulisList<String> list = new ArrayList<>(), bukanArrayList<String> list = new ArrayList<>(). Fleksibilitas untuk ganti implementasi tanpa mengubah kode lain.- Gunakan
removeIfbukan hapus saat iterasi — menghapus elemen saat for-each akan melemparConcurrentModificationException. PakairemoveIf()atauIterator.remove().Collectionsmenyediakan utilitas lengkap —sort,shuffle,reverse,binarySearch,frequency,unmodifiableList, dan banyak lagi — semuanya bekerja di semua implementasiList.