List #
List is the ordered collection most often used in everyday Kotlin programming. Kotlin strictly distinguishes between List (read-only) and MutableList (modifiable) — this difference isn’t just a convention, it’s encoded directly in the type system. This means when you receive a parameter of type List<T>, you have a compiler guarantee that you can’t accidentally modify its contents. Behind the scenes, both usually use Java’s ArrayList, but Kotlin wraps it with the right interface to ensure safety. This article covers List thoroughly: from creation and basic access to the entire arsenal of functional operations that make Kotlin so expressive in processing collection data.
Creating a List #
There are several ways to create a List depending on your needs:
// listOf() — immutable, type inferred from the elements
val fruits = listOf("Mangga", "Apel", "Jeruk", "Durian")
val numbers = listOf(1, 2, 3, 4, 5)
val mixed = listOf(1, "two", 3.0, true) // List<Any>
// emptyList() — empty list with an explicit type
val empty = emptyList<String>()
// listOfNotNull() — filters nulls automatically
val userInput: List<String?> = listOf("Budi", null, "Sari", null, "Ahmad")
val valid = listOfNotNull(*userInput.toTypedArray())
// or more idiomatically:
val valid2 = listOf("Budi", null, "Sari", null, "Ahmad").filterNotNull()
// buildList — builder DSL for flexible construction
val dynamic = buildList {
add("first")
addAll(listOf("second", "third"))
if (true) add("conditional")
repeat(3) { add("item-$it") }
}
println(dynamic)
// [first, second, third, conditional, item-0, item-1, item-2]
List vs MutableList
#
List is a read-only interface — it can only be read, not modified. MutableList extends List with write operations.
// List — read-only
val list: List<String> = listOf("Kotlin", "Java", "Python")
println(list[0]) // Kotlin
println(list.size) // 3
// list.add("Go") // ✗ error — List has no add() method
// list[0] = "Swift" // ✗ error — List can't be modified
// MutableList — can be read and modified
val mutableList: MutableList<String> = mutableListOf("Kotlin", "Java", "Python")
mutableList.add("Go") // add at the end
mutableList.add(1, "Swift") // add at a specific index
mutableList[0] = "Latest Kotlin" // replace an element
mutableList.removeAt(2) // remove at an index
mutableList.remove("Go") // remove by value
println(mutableList)
Principle: Expose as Narrowly as Possible #
// ANTI-PATTERN: exposing MutableList to outside code
class ShoppingCart {
val products: MutableList<String> = mutableListOf() // outside code can modify freely!
}
// CORRECT: store as MutableList internally, expose as List
class ShoppingCart {
private val _products: MutableList<String> = mutableListOf()
val products: List<String> get() = _products // read-only to the outside
fun add(product: String) { _products.add(product) }
fun remove(product: String) { _products.remove(product) }
}
Accessing Elements #
val languages = listOf("Kotlin", "Java", "Python", "Go", "Rust")
// Access by index — throws IndexOutOfBoundsException if out of range
println(languages[0]) // Kotlin
println(languages[4]) // Rust
// Safe access — returns null if the index is out of range
println(languages.getOrNull(10)) // null
println(languages.getOrElse(10) { "N/A" }) // N/A
// First and last elements
println(languages.first()) // Kotlin
println(languages.last()) // Rust
println(languages.firstOrNull()) // Kotlin (null if the list is empty)
println(languages.lastOrNull()) // Rust (null if the list is empty)
// First/last element matching a condition
println(languages.first { it.length > 4 }) // Kotlin
println(languages.last { it.length <= 3 }) // Go
println(languages.firstOrNull { it.startsWith("Z") }) // null
// Structure information
println(languages.size) // 5
println(languages.isEmpty()) // false
println(languages.isNotEmpty()) // true
println(languages.indices) // 0..4
println(languages.lastIndex) // 4
Modifying a MutableList #
val list = mutableListOf("A", "B", "C", "D", "E")
// Adding elements
list.add("F") // add at the end
list.add(2, "X") // insert at index 2
list.addAll(listOf("G", "H")) // add many at the end
list.addAll(0, listOf("Z1", "Z2")) // insert many at index 0
// Removing elements
list.remove("X") // remove by value (first occurrence found)
list.removeAt(0) // remove at an index
list.removeAll(listOf("G", "H")) // remove all present in the argument list
list.removeIf { it.startsWith("Z") } // remove elements matching a condition
// Replacing elements
list[0] = "Alpha" // replace at an index
list.set(1, "Beta") // same as above
// In-place sorting
val numbers = mutableListOf(5, 2, 8, 1, 9, 3)
numbers.sort() // ascending
println(numbers) // [1, 2, 3, 5, 8, 9]
numbers.sortDescending() // descending
numbers.sortBy { it % 3 } // sort by a criterion
numbers.shuffle() // randomize the order
// Empty the list
list.clear()
Search and Check Operations #
val products = listOf("Laptop", "Mouse", "Keyboard", "Monitor", "Headset")
// Check existence
println("Mouse" in products) // true (the in operator)
println(products.contains("Tablet")) // false
println(products.containsAll(listOf("Mouse", "Keyboard"))) // true
// Find positions
println(products.indexOf("Monitor")) // 3
println(products.lastIndexOf("Mouse")) // 1
println(products.indexOfFirst { it.length > 6 }) // 0 (Laptop)
println(products.indexOfLast { it.length <= 5 }) // 1 (Mouse)
// Find elements
println(products.find { it.startsWith("K") }) // Keyboard
println(products.findLast { it.contains("o") }) // Monitor
// Check conditions across all elements
println(products.all { it.isNotEmpty() }) // true — all non-empty
println(products.any { it.startsWith("Z") }) // false — none start with "Z"
println(products.none { it.length > 20 }) // true — none are >20 characters
println(products.count { it.length > 6 }) // 3 (Keyboard, Monitor, Headset)
Transformations — Producing a New List #
All functions in this section don’t modify the original list — they produce a new list.
map and Its Variants
#
val prices = listOf(50_000, 150_000, 75_000, 200_000)
// map — transform every element
val discountedPrices = prices.map { (it * 0.9).toInt() }
println(discountedPrices) // [45000, 135000, 67500, 180000]
// mapIndexed — transform with access to the index
val numbered = listOf("Apel", "Jeruk", "Mangga")
.mapIndexed { i, fruit -> "${i + 1}. $fruit" }
println(numbered) // [1. Apel, 2. Jeruk, 3. Mangga]
// mapNotNull — transform + filter nulls at once
val input = listOf("1", "two", "3", "four", "5")
val validNumbers = input.mapNotNull { it.toIntOrNull() }
println(validNumbers) // [1, 3, 5]
// flatMap — one-to-many transform then flatten
val sentences = listOf("halo dunia", "kotlin hebat")
val words = sentences.flatMap { it.split(" ") }
println(words) // [halo, dunia, kotlin, hebat]
// flatten — flatten a list of lists into a single list
val matrix = listOf(listOf(1, 2, 3), listOf(4, 5), listOf(6, 7, 8, 9))
println(matrix.flatten()) // [1, 2, 3, 4, 5, 6, 7, 8, 9]
filter and Its Variants
#
val scores = listOf(45, 78, 92, 55, 88, 61, 70, 95, 40)
val passed = scores.filter { it >= 70 }
println(passed) // [78, 92, 88, 70, 95]
val failed = scores.filterNot { it >= 70 }
println(failed) // [45, 55, 61, 40]
// filterIndexed — filter with access to the index
val evenIndices = scores.filterIndexed { i, _ -> i % 2 == 0 }
println(evenIndices) // [45, 92, 88, 70, 40] — indices 0, 2, 4, 6, 8
// partition — split into two lists at once
val (passed2, notPassed) = scores.partition { it >= 70 }
println("Passed: $passed2")
println("Failed: $notPassed")
// filterIsInstance — filter by type
val mixed: List<Any> = listOf(1, "two", 3.0, "four", 5, true)
val stringsOnly = mixed.filterIsInstance<String>()
println(stringsOnly) // [two, four]
Aggregation — Computing from a List #
val numbers = listOf(3, 1, 4, 1, 5, 9, 2, 6, 5, 3)
// Basic statistics
println(numbers.sum()) // 39
println(numbers.count()) // 10
println(numbers.min()) // 1
println(numbers.max()) // 9
println(numbers.average()) // 3.9
// sumOf, minOf, maxOf — for types that need transformation
data class Product(val name: String, val price: Int, val stock: Int)
val products = listOf(
Product("Laptop", 15_000_000, 5),
Product("Mouse", 250_000, 50),
Product("Keyboard", 500_000, 20)
)
println(products.sumOf { it.price }) // 15750000
println(products.minOf { it.price }) // 250000
println(products.maxOf { it.price }) // 15000000
println(products.maxByOrNull { it.stock }) // Product(name=Mouse, ...)
// reduce — accumulation without an initial value
val result = numbers.reduce { acc, value -> acc + value }
println(result) // 39
// fold — accumulation with an initial value
val sumWithBonus = numbers.fold(100) { acc, n -> acc + n }
println(sumWithBonus) // 139
// joinToString — combine into a String
val languages = listOf("Kotlin", "Java", "Python")
println(languages.joinToString(", ")) // Kotlin, Java, Python
println(languages.joinToString(prefix = "[", postfix = "]", separator = " | "))
// [Kotlin | Java | Python]
println(languages.joinToString { it.uppercase() }) // KOTLIN, JAVA, PYTHON
Sorting #
val names = listOf("Charlie", "Alice", "Bob", "Diana", "Eve")
// Sort — produce a new list (the original list doesn't change)
println(names.sorted()) // [Alice, Bob, Charlie, Diana, Eve]
println(names.sortedDescending()) // [Eve, Diana, Charlie, Bob, Alice]
println(names.sortedBy { it.length }) // [Bob, Eve, Alice, Diana, Charlie]
println(names.sortedByDescending { it.length }) // [Charlie, Diana, Alice, Bob, Eve]
// sortedWith — custom ordering with a Comparator
val sortedNames = names.sortedWith(compareBy({ it.length }, { it }))
println(sortedNames) // [Bob, Eve, Alice, Diana, Charlie]
// reversed — reverse the order
println(names.reversed()) // [Eve, Diana, Bob, Alice, Charlie]
data class Student(val name: String, val gpa: Double, val batch: Int)
val students = listOf(
Student("Budi", 3.85, 2022),
Student("Sari", 3.92, 2021),
Student("Ahmad", 3.71, 2022),
Student("Rina", 3.92, 2020)
)
// Sort by GPA descending, then name ascending
val sorted = students.sortedWith(compareByDescending<Student> { it.gpa }.thenBy { it.name })
sorted.forEach { println("${it.name}: ${it.gpa} (${it.batch})") }
// Rina: 3.92 (2020)
// Sari: 3.92 (2021)
// Budi: 3.85 (2022)
// Ahmad: 3.71 (2022)
Grouping #
data class Transaction(val category: String, val amount: Int)
val transactions = listOf(
Transaction("Food", 50_000),
Transaction("Transport", 30_000),
Transaction("Food", 75_000),
Transaction("Entertainment", 120_000),
Transaction("Transport", 25_000),
Transaction("Food", 45_000)
)
// groupBy — group into a Map<K, List<V>>
val byCategory = transactions.groupBy { it.category }
byCategory.forEach { (cat, list) ->
println("$cat: ${list.size} transactions, total Rp${\"%,d\".format(list.sumOf { it.amount })}")
}
// Food: 3 transactions, total Rp170,000
// Transport: 2 transactions, total Rp55,000
// Entertainment: 1 transaction, total Rp120,000
// groupingBy().eachCount() — count per group
val countPerCategory = transactions.groupingBy { it.category }.eachCount()
println(countPerCategory) // {Food=3, Transport=2, Entertainment=1}
// chunked — split into chunks of a given size
val pages = listOf(1..20).flatMap { it.toList() }.chunked(5)
pages.forEach { println(it) }
// [1, 2, 3, 4, 5]
// [6, 7, 8, 9, 10]
// [11, 12, 13, 14, 15]
// [16, 17, 18, 19, 20]
// windowed — sliding windows
val data = listOf(1.0, 2.0, 3.0, 4.0, 5.0)
val movingAverage = data.windowed(3) { it.average() }
println(movingAverage) // [2.0, 3.0, 4.0]
Conversion Between Collections #
val list = listOf("Budi", "Sari", "Ahmad", "Budi", "Rina", "Sari")
// To Set — remove duplicates
val unique = list.toSet()
println(unique) // [Budi, Sari, Ahmad, Rina]
// To MutableList — create a modifiable copy
val copy = list.toMutableList()
copy.add("Extra")
// To Map — transform into key-value pairs
val nameLengths = list.distinct().associateWith { it.length }
println(nameLengths) // {Budi=4, Sari=4, Ahmad=5, Rina=4}
// zip — pair up two lists
val names = listOf("Budi", "Sari", "Ahmad")
val scores = listOf(85, 92, 78)
val pairs = names.zip(scores)
println(pairs) // [(Budi, 85), (Sari, 92), (Ahmad, 78)]
// unzip — split a list of pairs into two lists
val (namesOnly, scoresOnly) = pairs.unzip()
println(namesOnly) // [Budi, Sari, Ahmad]
println(scoresOnly) // [85, 92, 78]
// distinct — remove duplicates (preserve order)
println(list.distinct()) // [Budi, Sari, Ahmad, Rina]
println(list.distinctBy { it.first() }) // [Budi, Sari, Ahmad, Rina]
Sequence — For Performance on Large Data #
By default, Kotlin collection operations are eager — every operation executes immediately and produces an intermediate list. For long pipelines on large data, Sequence provides more efficient lazy evaluation.
// ANTI-PATTERN: eager — creates 3 intermediate lists for 1 million elements
val eagerResult = (1..1_000_000)
.toList()
.filter { it % 2 == 0 } // list 1: 500,000 elements
.map { it * it } // list 2: 500,000 elements
.take(5) // list 3: 5 elements
println(eagerResult)
// CORRECT: sequence — only processes what's needed
val lazyResult = (1..1_000_000)
.asSequence()
.filter { it % 2 == 0 } // lazy — not yet executed
.map { it * it } // lazy — not yet executed
.take(5) // lazy — not yet executed
.toList() // now executed — only processes until 10 elements!
println(lazyResult) // [4, 16, 36, 64, 100]
USE List (eager) if:
✓ Small to medium data (thousands of elements)
✓ Short pipelines (1-2 operations)
✓ You need random access to intermediate elements
USE Sequence (lazy) if:
✓ Very large data (hundreds of thousands or more)
✓ Long pipelines (3+ operations)
✓ Operations that may stop early (take, find, first)
✓ Reading data from a stream/file line by line
Summary #
listOfvsmutableListOf— uselistOfas the default. Switch tomutableListOfonly if the list actually needs to be modified after creation. Always expose asListto the outside, store asMutableListinternally.- Safe access with
getOrNullandgetOrElse— avoidIndexOutOfBoundsExceptionby usinggetOrNull(i)instead oflist[i]when the index may be out of range.mapNotNullfor transform + filter at once — more efficient thanmapthenfilterNotNull. Very useful for parsing input that may be invalid.partitionfor conditional splitting —val (passed, failed) = scores.partition { it >= 70 }is cleaner than two separatefiltercalls.groupByfor per-group aggregation — produces aMap<K, List<V>>that can be processed directly. Combine withsumOf,count,maxByOrNullfor aggregate reports.joinToStringfor string representation — far more flexible than a manual loop. Supportsprefix,postfix,separator, and element transformation at once.zipandunzipfor paired lists — pair two lists withzipand split them back withunzip. Useful for processing correlated data.Sequencefor large data — add.asSequence()before long pipelines on large data for lazy evaluation that’s far more memory- and time-efficient.