Pilihan koleksi sangat memengaruhi performance. ArrayList cepat untuk random access tetapi lambat untuk insert di tengah. LinkedList kebalikannya. HashMap memberi lookup O(1) tetapi tidak menjaga urutan, sementara TreeMap menjaga urutan tetapi lookup O(log n). Pada bab ini kita akan membandingkan keempat implementasi dengan step counter, mendemokan manfaat initialCapacity, dan menyusun tabel ringkasan Big-O untuk acuan cepat.
ArrayList vs LinkedList #
ArrayList menyimpan elemen di array backing yang otomatis di-resize bila penuh. LinkedList menyimpan elemen di node terpisah dengan pointer next/prev. Perbedaan struktur ini menyebabkan perbedaan performance yang dramatis pada operasi tertentu.
| Operasi | ArrayList | LinkedList |
|---|---|---|
add di akhir | O(1) amortized (resize jarang) | O(1) (Node baru) |
get(i) random | O(1) (array index) | O(n) (traverse) |
insert(i) di tengah | O(n) (array copy) | O(1) (relink) |
remove(i) di tengah | O(n) (array copy) | O(1) (relink) |
| Iteration | Cepat (cache locality) | Lambat (pointer chasing) |
| Memori per elemen | 4-8 bytes | 24-32 bytes (Node overhead) |
Untuk mayoritas kasus, ArrayList lebih cepat karena cache locality. LinkedList hanya unggul bila sering insert/remove di tengah dan jarang random access.
HashMap vs TreeMap #
HashMap memakai hash table dengan bucket. Lookup rata-rata O(1), worst case O(n) bila banyak collision. TreeMap memakai red-black tree yang menjaga key terurut. Lookup O(log n).
| Karakteristik | HashMap | TreeMap |
|---|---|---|
| Lookup rata-rata | O(1) | O(log n) |
| Iteration order | Tidak terjamin | Sorted by key |
| Memori overhead | Table + Entry per item | Tree node per item |
| Null key | Diizinkan (1 null) | Tidak diizinkan |
| Cocok untuk | Cache, lookup cepat | Range query, sorted iteration |
Pilih TreeMap hanya bila perlu iterasi terurut atau range query (subMap, headMap, tailMap). Untuk kasus lain, HashMap selalu lebih cepat.
Pre-size dengan initialCapacity #
ArrayList dan HashMap mempunyai konstruktor yang menerima initialCapacity. Bila kita tahu ukuran akhir collection, set capacity awal agar tidak ada resize.
// Tanpa pre-size: ArrayList default cap = 16
val list1 = ArrayList<Int>()
for (i in 1..1000) list1.add(i)
// ArrayList akan resize ~6 kali (16 → 32 → 64 → ... → 1024)
// Dengan pre-size: kapasitas awal sudah cukup
val list2 = ArrayList<Int>(1000)
for (i in 1..1000) list2.add(i)
// Tidak ada resize, hemat 6x array copyUntuk HashMap, aturan umum: set initialCapacity = expectedSize / 0.75 + 1 (default load factor 0.75) agar tidak terjadi rehash. Bila expected size 1000, set capacity ke 1334.
Demo Step-Counting #
Cuplikan berikut membandingkan keempat koleksi dengan step counter. Untuk random access LinkedList, simulasi menghitung ~500 langkah traversal. Untuk TreeMap, simulasi menghitung ~10 langkah (log2 1000):
// Demo collection performance: ArrayList vs LinkedList, HashMap vs TreeMap,
// pre-size dengan initialCapacity. Menggunakan step counter (bukan nanotime).
import java.util.TreeMap
class CollectionProfiler {
var steps: Long = 0
var allocations: Long = 0
fun reset() { steps = 0; allocations = 0 }
fun step(n: Long = 1) { steps += n }
fun alloc(n: Long = 1) { allocations += n }
fun report(label: String): String {
return " %-30s steps=%-12d allocations=%-6d".format(label, steps, allocations)
}
}
fun main() {
println("=== Collection Performance ===")
println()
println("Catatan: 'steps' = operasi internal (array copy, traversal).")
println(" 'allocations' = objek tambahan yang dibuat.")
println()
val profiler = CollectionProfiler()
val N = 1000
// === 1. Append ke ArrayList ===
println("=== 1. ArrayList append (cap awal = 16) ===")
profiler.reset()
val arrayList = ArrayList<Int>(16) // default small capacity
for (i in 1..N) {
arrayList.add(i)
profiler.step()
// Setiap kali array internal full, Kotlin reallocate
// Hitung berapa kali resize terjadi
}
var arrayResizes = 0
var cap = 16
while (cap < N) { cap *= 2; arrayResizes++ }
profiler.alloc(arrayResizes.toLong())
println(profiler.report("ArrayList (no presize)"))
println(" Resize terjadi: $arrayResizes kali (array copy)")
println()
// === 2. ArrayList dengan initialCapacity ===
println("=== 2. ArrayList append (pre-size = $N) ===")
profiler.reset()
val sizedList = ArrayList<Int>(N) // pre-size yang tepat
for (i in 1..N) {
sizedList.add(i)
profiler.step()
}
// Tidak ada resize karena kapasitas sudah cukup
println(profiler.report("ArrayList (presized)"))
println(" Resize terjadi: 0 kali (kapasitas awal cukup)")
println()
// === 3. LinkedList append ===
println("=== 3. LinkedList append ===")
profiler.reset()
val linkedList = java.util.LinkedList<Int>()
for (i in 1..N) {
linkedList.add(i)
profiler.step()
profiler.alloc() // Node object baru per append
}
println(profiler.report("LinkedList append"))
println(" Alokasi: $N Node objects (overhead per elemen)")
println()
// === 4. Random access: ArrayList vs LinkedList ===
println("=== 4. Random access (index 500) ===")
profiler.reset()
repeat(100) {
arrayList[500] // O(1) — direct array access
profiler.step()
}
println(profiler.report("ArrayList.get(500)"))
profiler.reset()
repeat(100) {
// LinkedList.get(i) harus traverse dari head
linkedList[500] // O(n) — traverse
profiler.step(500) // 500 langkah traversal
}
println(profiler.report("LinkedList.get(500)"))
println()
// === 5. HashMap vs TreeMap lookup ===
println("=== 5. HashMap vs TreeMap lookup (key='key-500') ===")
val hashMap = HashMap<String, Int>(N)
val treeMap = TreeMap<String, Int>()
for (i in 1..N) {
hashMap["key-$i"] = i
treeMap["key-$i"] = i
}
profiler.reset()
repeat(100) {
hashMap["key-500"] // O(1) rata-rata
profiler.step() // 1 langkah hash lookup
}
println(profiler.report("HashMap.get(key-500)"))
profiler.reset()
repeat(100) {
treeMap["key-500"] // O(log n) - traverse pohon
profiler.step(10) // ~log2(1000) ≈ 10 langkah
}
println(profiler.report("TreeMap.get(key-500)"))
println()
// === 6. Iteration: ArrayList vs LinkedList ===
println("=== 6. Iteration (sum semua elemen) ===")
profiler.reset()
var sum1 = 0
for (x in arrayList) { sum1 += x; profiler.step() }
println(profiler.report("ArrayList.iterate"))
profiler.reset()
var sum2 = 0
for (x in linkedList) { sum2 += x; profiler.step() }
println(profiler.report("LinkedList.iterate"))
println()
// === 7. Tabel ringkasan ===
println("=== 7. Ringkasan Big-O ===")
val tabel = listOf(
Triple("ArrayList.add", "O(1) amortized", "Resize ganda bila full"),
Triple("ArrayList.get(i)", "O(1)", "Direct array index"),
Triple("LinkedList.add", "O(1)", "Alokasi Node baru"),
Triple("LinkedList.get(i)", "O(n)", "Traverse dari head"),
Triple("HashMap.get(k)", "O(1) avg", "Hash + bucket"),
Triple("TreeMap.get(k)", "O(log n)", "BST traversal"),
Triple("HashMap.iterate", "O(n)", "Sequential bucket"),
Triple("TreeMap.iterate", "O(n) in-order", "In-order traversal")
)
tabel.forEach { (op, bigO, keterangan) ->
println(" %-25s %-15s %s".format(op, bigO, keterangan))
}
println()
println("Sum ArrayList: $sum1, Sum LinkedList: $sum2 (sama = ${sum1 == sum2})")
}Output yang Dihasilkan:
=== Collection Performance ===
Catatan: 'steps' = operasi internal (array copy, traversal).
'allocations' = objek tambahan yang dibuat.
=== 1. ArrayList append (cap awal = 16) ===
ArrayList (no presize) steps=1000 allocations=6
Resize terjadi: 6 kali (array copy)
=== 2. ArrayList append (pre-size = 1000) ===
ArrayList (presized) steps=1000 allocations=0
Resize terjadi: 0 kali (kapasitas awal cukup)
=== 3. LinkedList append ===
LinkedList append steps=1000 allocations=1000
Alokasi: 1000 Node objects (overhead per elemen)
=== 4. Random access (index 500) ===
ArrayList.get(500) steps=100 allocations=0
LinkedList.get(500) steps=50000 allocations=0
=== 5. HashMap vs TreeMap lookup (key='key-500') ===
HashMap.get(key-500) steps=100 allocations=0
TreeMap.get(key-500) steps=1000 allocations=0
=== 6. Iteration (sum semua elemen) ===
ArrayList.iterate steps=1000 allocations=0
LinkedList.iterate steps=1000 allocations=0
=== 7. Ringkasan Big-O ===
ArrayList.add O(1) amortized Resize ganda bila full
ArrayList.get(i) O(1) Direct array index
LinkedList.add O(1) Alokasi Node baru
LinkedList.get(i) O(n) Traverse dari head
HashMap.get(k) O(1) avg Hash + bucket
TreeMap.get(k) O(log n) BST traversal
HashMap.iterate O(n) Sequential bucket
TreeMap.iterate O(n) in-order In-order traversal
Sum ArrayList: 500500, Sum LinkedList: 500500 (sama = true)Mari kita bedah hasil pengukuran di atas:
- ArrayList tanpa pre-size: terjadi 6 kali resize (16 → 32 → 64 → 128 → 256 → 512 → 1024). Setiap resize meng-copy seluruh elemen ke array baru, sehingga total copy adalah ~1000 + ~1000 = 2000 elemen.
- ArrayList dengan pre-size: tidak ada resize. Hemat 6x array copy. Bila ukuran akhir diketahui, selalu pre-size.
- LinkedList append: setiap
addmengalokasikan satuNodeobject (24-32 bytes di JVM 64-bit). Untuk 1000 elemen, ada 1000 Node + array references. - Random access
arrayList[500]: O(1) — langsung akses array index. Untuk LinkedList, harus traverse 500 node dari head, jadi O(n). - HashMap lookup: O(1) rata-rata — hash function menghasilkan index bucket, lalu cek equality. Untuk TreeMap, harus traverse pohon BST, jadi O(log n) ≈ 10 langkah untuk 1000 elemen.
- Iteration: kedua koleksi O(n), tetapi ArrayList lebih cepat di praktik karena cache locality (elemen bersebelahan di memori). LinkedList mengalami cache miss karena pointer chasing.
Cache Locality #
CPU modern mempunyai cache L1/L2/L3 yang berukuran kecil tetapi sangat cepat. Bila elemen yang diakses berdekatan di memori (seperti array), cache hit rate tinggi. Bila elemen tersebar (seperti linked list node), cache miss sering terjadi dan CPU menunggu data dari RAM (~100 ns).
// ArrayList: cache-friendly
for (x in arrayList) sum += x // 1 cache line per 16 Int (64 bytes / 4 bytes)
// LinkedList: cache-unfriendly
for (x in linkedList) sum += x // cache miss per node (data tersebar)Untuk 1 juta elemen, perbedaan ini bisa mencapai 5-10x lipat waktu eksekusi, walaupun Big-O keduanya O(n).
Referensi Lanjutan #
- Collections overview — ringkasan tipe koleksi Kotlin beserta kompleksitas operasi umumnya
- Sequences
— dokumentasi
Sequenceuntuk pemrosesan lazy yang menghindari koleksi antara
Selanjutnya #
Untuk operasi chain seperti map.filter.map, koleksi List membuat intermediate list di setiap step. Bab berikutnya membahas Sequence sebagai alternatif lazy yang hemat alokasi. Materi tersebut ada di chapter Sequence Performance.