Skip to content

Kotlin Collections — Lists, Sets, and Maps Guide

DodaTech Updated 2026-06-28 9 min read

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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. Not using distinct() when needed: Processing data from APIs often produces duplicates. Use distinct() or toSet() to remove them before further processing.

  6. Using == instead of === on collections: == checks structural equality (same elements). === checks referential equality (same object). Understand which one you need.

Practice Questions

  1. 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.

  1. 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().

  1. 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.

  1. 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.

  1. 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 is the default List implementation in Kotlin?

listOf returns an optimized internal implementation (ArraysArrayList). mutableListOf returns ArrayList unless targeting JavaScript (returns Array).

Are Kotlin collections compatible with Java collections?

Yes. Kotlin collections map directly to Java collection interfaces. Kotlin's List is java.util.List, MutableList is java.util.ArrayList, etc.

What is the difference between sortedBy and sortBy?

sortedBy is an extension on any collection that returns a new sorted list. sortBy is an extension on MutableList that sorts in place.

How do I create an empty collection?

Use emptyList(), emptySet(), or emptyMap(). These return singleton immutable instances that do not allocate new objects.

Can I use Java streams with Kotlin collections?

Yes, but Kotlin's built-in collection functions (filter, map, etc.) are preferred because they do not require a stream conversion and are more idiomatic.

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