Kotlin Collections — Lists, Sets, and Maps Guide
In this tutorial, you will learn about Kotlin Collections. We cover key concepts, practical examples, and best practices to help you master this topic.
Kotlin collections are divided into mutable and immutable interfaces with List, Set, and Map variants, and provide rich functional operations including filter, map, flatMap, reduce, and groupBy for declarative data processing.
What You'll Learn
- Create and manipulate lists (ArrayList, mutableListOf, listOf)
- Work with sets for unique elements (hashSetOf, mutableSetOf, setOf)
- Use maps for key-value pairs (hashMapOf, mutableMapOf, mapOf)
- Understand mutable versus immutable (read-only) collection interfaces
- Iterate with for loops, forEach, and iterators
- Apply functional operations: filter, map, flatMap, reduce, fold
- Sort, group, and partition collections
- Convert between collection types
Why It Matters
Collections are at the heart of almost every program. You will store user lists, product catalogs, API responses, configuration maps, and more. Kotlin's collection library is more expressive than Java's. It provides both mutable and immutable interfaces, a comprehensive set of functional operations, and seamless interop with Java Collections. The ability to chain filter, map, and reduce operations makes data processing code shorter and more readable.
Real-World Use
DodaTech processes malware signature lists using Kotlin's collection operations. Filter operations remove inactive signatures. Map operations transform raw data into typed objects. GroupBy organizes signatures by severity level. The immutable-by-default approach prevents accidental data corruption in concurrent Android services.
Learning Path
flowchart LR A[Null Safety] --> B[Collections\nYou are here] B --> C[Lambdas] style B fill:#f90,color:#fff
Lists
A List is an ordered collection with indexed access. Kotlin distinguishes between read-only List and MutableList.
fun main() {
// Immutable list (read-only)
val fruits = listOf("Apple", "Banana", "Cherry", "Apple")
println(fruits.size) // Output: 4
println(fruits[0]) // Output: Apple
println(fruits.contains("Banana")) // Output: true
// Mutable list
val mutableFruits = mutableListOf("Apple", "Banana")
mutableFruits.add("Cherry")
mutableFruits.add(0, "Apricot")
mutableFruits.remove("Apple")
mutableFruits[1] = "Blueberry"
println(mutableFruits) // Output: [Apricot, Blueberry, Cherry]
// ArrayList (Java-compatible)
val arrayList = arrayListOf(1, 2, 3)
arrayList.addAll(listOf(4, 5))
println(arrayList) // Output: [1, 2, 3, 4, 5]
}
Output: Immutable lists cannot be modified. Mutable lists support add, remove, and indexed updates.
Sets
A Set is an unordered collection with unique elements.
fun main() {
// Immutable set
val numbers = setOf(1, 2, 3, 3, 2, 1)
println(numbers) // Output: [1, 2, 3]
println(numbers.size) // Output: 3 (duplicates removed)
// Mutable set
val mutableSet = mutableSetOf("A", "B", "C")
mutableSet.add("D")
mutableSet.add("A") // No effect, already present
mutableSet.remove("B")
println(mutableSet) // Output: [A, C, D]
// HashSet (faster, no order guarantee)
val hashSet = hashSetOf(3, 1, 2, 3)
println(hashSet) // Output might be [1, 2, 3] (order not guaranteed)
// LinkedHashSet (insertion order preserved)
val linkedSet = linkedSetOf("X", "Y", "Z")
linkedSet.add("W")
println(linkedSet) // Output: [X, Y, Z, W]
}
Output: Sets automatically remove duplicates. HashSet offers better performance but no ordering guarantee.
Maps
A Map stores key-value pairs. Keys are unique.
fun main() {
// Immutable map
val scores = mapOf(
"Alice" to 95,
"Bob" to 87,
"Charlie" to 92
)
println(scores["Alice"]) // Output: 95
println(scores["Unknown"]) // Output: null
println(scores.getOrDefault("Unknown", 0)) // Output: 0
// Mutable map
val config = mutableMapOf("theme" to "dark", "fontSize" to "14")
config["language"] = "en"
config["theme"] = "light"
config.remove("fontSize")
println(config) // Output: {theme=light, language=en}
// HashMap (general-purpose)
val hashMap = hashMapOf("key1" to "value1")
hashMap["key2"] = "value2"
// Iterating over a map
for ((name, score) in scores) {
println("$name: $score")
}
// Output: Alice: 95 / Bob: 87 / Charlie: 92
}
Output: Maps provide key-based access. The bracket operator returns null for missing keys.
Mutable versus Immutable
Kotlin enforces immutability through interfaces, not runtime checks.
fun main() {
// Read-only reference, mutable backing
val readOnly: List<Int> = mutableListOf(1, 2, 3)
// readOnly.add(4) // Compilation error (read-only interface)
// The backing list can still be modified through the mutable reference
val mutable: MutableList<Int> = readOnly as MutableList<Int>
mutable.add(4)
println(readOnly) // Output: [1, 2, 3, 4]
// True immutability with listOf
val trulyImmutable = listOf(1, 2, 3)
// Casting to MutableList would throw ClassCastException at runtime
}
Output: The read-only interface prevents modification through that reference, but the underlying list may still be mutable.
Use listOf, setOf, and mapOf for truly immutable collections. Use toList(), toSet(), toMap() to create defensive copies.
Functional Operations
Kotlin collections support a rich set of functional operations.
data class Product(val name: String, val price: Double, val category: String)
fun main() {
val products = listOf(
Product("Laptop", 999.99, "Electronics"),
Product("Mouse", 25.50, "Electronics"),
Product("Book", 15.99, "Media"),
Product("Keyboard", 75.00, "Electronics"),
Product("Notebook", 4.99, "Media")
)
// Filter: select elements matching a condition
val electronics = products.filter { it.category == "Electronics" }
println("Electronics: ${electronics.size}") // Output: Electronics: 3
// Map: transform each element
val names = products.map { it.name }
println(names) // Output: [Laptop, Mouse, Book, Keyboard, Notebook]
// Sorted
val sortedByName = products.sortedBy { it.name }
println(sortedByName.first().name) // Output: Book
// GroupBy
val byCategory = products.groupBy { it.category }
println(byCategory.keys) // Output: [Electronics, Media]
// Partition: split into two lists
val (expensive, cheap) = products.partition { it.price > 50.0 }
println("Expensive: ${expensive.size}, Cheap: ${cheap.size}")
// Output: Expensive: 3, Cheap: 2
// FlatMap: flatten nested collections
val words = listOf("hello world", "kotlin is fun", "collections")
val allWords = words.flatMap { it.split(" ") }
println(allWords) // Output: [hello, world, kotlin, is, fun, collections]
// Distinct
val withDuplicates = listOf(1, 2, 2, 3, 1, 3)
println(withDuplicates.distinct()) // Output: [1, 2, 3]
}
Output: Each functional operation returns a new collection without modifying the original.
Reduce and Fold
Reduce and fold aggregate collection elements into a single value.
fun main() {
val numbers = listOf(1, 2, 3, 4, 5)
// Sum with reduce
val sum = numbers.reduce { acc, n -> acc + n }
println(sum) // Output: 15
// Reduce with different operation
val product = numbers.reduce { acc, n -> acc * n }
println(product) // Output: 120
// Fold with initial value
val foldSum = numbers.fold(10) { acc, n -> acc + n }
println(foldSum) // Output: 25 (10 + 1 + 2 + 3 + 4 + 5)
// Reduce throws on empty list
val empty = emptyList<Int>()
// empty.reduce { a, b -> a + b } // Throws UnsupportedOperationException
// Fold handles empty lists gracefully
val emptyFold = empty.fold(0) { acc, n -> acc + n }
println(emptyFold) // Output: 0
// Count, sum, average (built-in)
println(numbers.count()) // Output: 5
println(numbers.sum()) // Output: 15
println(numbers.average()) // Output: 3.0
}
Output: reduce starts with the first element. fold starts with a provided initial value and works on empty lists.
Converting Between Collection Types
fun main() {
val list = listOf(3, 1, 2, 1, 3)
// List to Set (removes duplicates)
val set = list.toSet()
println(set) // Output: [3, 1, 2]
// Set to List
val backToList = set.toList()
println(backToList) // Output: [3, 1, 2]
// List to Map (with key selector)
val map = list.map { it to it.toString() }.toMap()
println(map) // Output: {3=3, 1=1, 2=2}
// Mutable copy of an immutable collection
val mutableList = list.toMutableList()
mutableList.add(4)
println(mutableList) // Output: [3, 1, 2, 1, 3, 4]
}
Output: Conversion functions create new collections of the target type.
Common Mistakes
Assuming listOf returns an immutable list at runtime: listOf returns a list that cannot be modified through any reference. Casting it to MutableList throws ClassCastException.
Forgetting that map values are nullable: The bracket accessor on Map returns null for missing keys. Use getValue() (throws exception) or getOrDefault() (returns default) when you need non-null guarantees.
Modifying a collection while iterating with forEach: Use a mutable Iterator's remove() method or collect results in a new list instead of modifying in-place during iteration.
Confusing sortedBy and sortBy: sortedBy returns a new sorted list. sortBy sorts the mutable collection in place. Using sortedBy on a MutableList returns a new list without modifying the original.
Not using distinct() when needed: Processing data from APIs often produces duplicates. Use distinct() or toSet() to remove them before further processing.
Using == instead of === on collections: == checks structural equality (same elements). === checks referential equality (same object). Understand which one you need.
Practice Questions
- What is the difference between listOf and mutableListOf?
Answer: listOf returns a read-only List that cannot be modified. mutableListOf returns a MutableList that supports add, remove, and update operations.
- How do you remove duplicates from a list?
Answer: Call .distinct() which returns a new list with duplicates removed, or convert to a set with .toSet().
- What does the partition function do?
Answer: It splits a collection into two lists based on a predicate. The first list contains elements that match the predicate, the second contains elements that do not.
- How is reduce different from fold?
Answer: reduce uses the first element as the initial accumulator and throws on empty collections. fold takes an explicit initial value and returns it for empty collections.
- Challenge: Given a list of strings, group them by their first character, then for each group, return the count of strings and the longest string. Print the results sorted by group key.
Answer:
fun main() {
val words = listOf("apple", "banana", "avocado", "blueberry", "cherry", "apricot", "cranberry")
val result = words
.groupBy { it.first() }
.map { (key, items) ->
key to Pair(items.size, items.maxByOrNull { it.length } ?: "")
}
.sortedBy { it.first }
for ((letter, (count, longest)) in result) {
println("$letter: $count words, longest = $longest")
}
}
Output: Each letter group shows the word count and the longest word in that group.
Mini Project
Create a product inventory manager. Requirements:
- Define a data class Product with id, name, price, and category
- Store products in a mutable list
- Implement functions to: add, remove, update price, find by category
- Implement reporting: total value, average price, most expensive product, count by category
- Use filter, map, groupBy, reduce, and sortedBy
- Handle empty inventory gracefully with safe calls and default values
This project consolidates all collection operations in a practical scenario.
FAQ
What's Next
After mastering collections, learn lambdas for writing functional-style operations. You can also explore classes to model real-world objects with object-oriented programming.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro