cyberangles guide

A Step-by-Step Guide to Kotlin Collections

In Kotlin, a **collection** is a group of related objects. The Kotlin Standard Library provides a comprehensive API for working with collections, designed to be: - **Concise**: Leverage Kotlin’s extension functions for readable, one-line operations. - **Safe**: Enforce immutability by default to prevent accidental data modification. - **Interoperable**: Seamlessly work with Java collections (e.g., `java.util.ArrayList`). At their core, Kotlin collections are divided into two broad categories: **immutable** (read-only) and **mutable** (read-write). This distinction is critical for writing robust code, as immutability reduces bugs related to unintended side effects.

Collections are fundamental in programming—they help organize, store, and manipulate groups of objects efficiently. Kotlin, a modern JVM language, provides a rich set of collection APIs that simplify working with data structures like lists, sets, and maps. Unlike Java, Kotlin collections emphasize immutability by default, offer concise syntax, and include powerful extension functions for common operations (e.g., filtering, mapping, and grouping).

Whether you’re a beginner or transitioning from Java, this guide will walk you through Kotlin collections step-by-step, from basic concepts to advanced usage. By the end, you’ll master how to choose, create, and manipulate collections effectively in Kotlin.

Table of Contents

  1. Introduction to Kotlin Collections
  2. Collection Hierarchy: Immutable vs. Mutable
  3. Core Collection Types
  4. Common Operations on Collections
  5. Advanced: Sequences for Lazy Evaluation
  6. Best Practices for Using Collections
  7. References

2. Collection Hierarchy: Immutable vs. Mutable

Kotlin’s collection hierarchy is built around a few key interfaces, with separate branches for immutable and mutable collections. Here’s a simplified overview:

Immutable Collections

These are read-only: you cannot add, remove, or modify elements after creation. They are defined by interfaces like List, Set, and Map (without the Mutable prefix).

Mutable Collections

These support modification: you can add, remove, or update elements. They extend immutable interfaces and add mutation methods (e.g., add(), remove()). Mutable counterparts include MutableList, MutableSet, and MutableMap.

Core Hierarchy Diagram

Iterable  
├── Collection  
│   ├── List (immutable) → MutableList (mutable)  
│   └── Set (immutable) → MutableSet (mutable)  
└── Map (immutable) → MutableMap (mutable)  // Map is separate from Collection  
  • Iterable: The root interface for all collections. It provides an iterator() to traverse elements.
  • Collection: Extends Iterable and adds basic operations like size, contains(), and isEmpty().
  • List/Set: Extend Collection with type-specific behavior (order for List, uniqueness for Set).
  • Map: A separate hierarchy for key-value pairs (not a Collection, as it doesn’t hold “elements” directly).

3. Core Collection Types

Let’s dive into the most common collection types: List, Set, and Map. For each, we’ll cover immutable and mutable variants, common implementations, and key operations.

3.1 Lists: Ordered Collections with Duplicates

A List is an ordered collection that allows duplicate elements. Elements are accessed by index (0-based).

Immutable Lists

Created with listOf() (empty or varargs) or emptyList() (for empty lists). They cannot be modified after creation.

// Empty immutable list  
val emptyList: List<Int> = emptyList()  

// List with elements (inferred type: List<String>)  
val fruits = listOf("Apple", "Banana", "Apple", "Orange")  

// Access elements by index  
println(fruits[0])  // Output: Apple  
println(fruits[2])  // Output: Apple (duplicate allowed)  

// Attempting to modify throws a compile error  
// fruits.add("Grapes")  // Error: Unresolved reference: add  

Mutable Lists

Created with mutableListOf(), arrayListOf() (backed by ArrayList), or listOf().toMutableList(). They support adding, removing, and updating elements.

// Mutable list (inferred type: MutableList<Int>)  
val numbers = mutableListOf(1, 2, 3)  

// Add elements  
numbers.add(4)  
numbers.addAll(listOf(5, 6))  
println(numbers)  // Output: [1, 2, 3, 4, 5, 6]  

// Update element at index  
numbers[1] = 20  
println(numbers)  // Output: [1, 20, 3, 4, 5, 6]  

// Remove elements  
numbers.remove(3)  // Remove by value  
numbers.removeAt(0)  // Remove by index  
println(numbers)  // Output: [20, 4, 5, 6]  

3.2 Sets: Unordered Collections with Unique Elements

A Set is an unordered collection that does not allow duplicate elements. If you add a duplicate, it is ignored.

Immutable Sets

Created with setOf() or emptySet().

// Set with elements (duplicates are automatically removed)  
val uniqueFruits = setOf("Apple", "Banana", "Apple", "Orange")  
println(uniqueFruits)  // Output: [Apple, Banana, Orange] (order not guaranteed)  

// Check membership  
println("Banana" in uniqueFruits)  // Output: true  

Mutable Sets

Created with mutableSetOf(), hashSetOf() (backed by HashSet), linkedSetOf() (preserves insertion order), or sortedSetOf() (sorted by natural order).

// HashSet (no guaranteed order)  
val hashSet = hashSetOf(3, 1, 2)  
println(hashSet)  // Output: [1, 2, 3] (order may vary)  

// LinkedHashSet (preserves insertion order)  
val linkedSet = linkedSetOf(3, 1, 2)  
println(linkedSet)  // Output: [3, 1, 2]  

// SortedSet (sorted by natural order)  
val sortedSet = sortedSetOf(3, 1, 2)  
println(sortedSet)  // Output: [1, 2, 3]  

// Add/remove elements  
linkedSet.add(4)  
linkedSet.remove(1)  
println(linkedSet)  // Output: [3, 2, 4]  

3.3 Maps: Key-Value Pair Collections

A Map stores key-value pairs, where each key is unique. Keys and values can be of any type, and a key maps to exactly one value.

Immutable Maps

Created with mapOf() (pairs via to infix function) or emptyMap().

// Map with key-value pairs (inferred type: Map<String, Int>)  
val ages = mapOf(  
    "Alice" to 30,  
    "Bob" to 25,  
    "Charlie" to 35  
)  

// Access values by key  
println(ages["Alice"])  // Output: 30  
println(ages.getOrDefault("Dave", 0))  // Output: 0 (default if key not found)  

// Iterate over entries  
for ((name, age) in ages) {  
    println("$name is $age years old")  
}  

Mutable Maps

Created with mutableMapOf(), hashMapOf(), linkedMapOf() (preserves insertion order), or sortedMapOf() (sorted by key).

// Mutable map (insertion order preserved with linkedMapOf)  
val mutableAges = linkedMapOf(  
    "Alice" to 30,  
    "Bob" to 25  
)  

// Add/update entries  
mutableAges["Charlie"] = 35  // Add new key  
mutableAges["Bob"] = 26      // Update existing key  

// Remove entry  
mutableAges.remove("Alice")  

println(mutableAges)  // Output: {Bob=26, Charlie=35}  

4. Common Operations on Collections

Kotlin’s strength lies in its rich set of extension functions for collections. These functions let you transform, filter, and analyze data with minimal code. Below are key operations grouped by use case.

Transforming: map, flatMap

  • map: Transforms each element using a lambda and returns a new collection.
  • flatMap: Transforms each element into a collection, then flattens all results into a single list.
val numbers = listOf(1, 2, 3, 4)  

// map: Square each number  
val squared = numbers.map { it * it }  
println(squared)  // Output: [1, 4, 9, 16]  

// flatMap: Split strings into characters, then flatten  
val words = listOf("Hello", "World")  
val chars = words.flatMap { it.toList() }  
println(chars)  // Output: [H, e, l, l, o, W, o, r, l, d]  

Filtering: filter, take, drop

  • filter: Returns elements that match a condition.
  • take(n): Returns the first n elements.
  • drop(n): Returns elements after skipping the first n.
val numbers = listOf(1, 2, 3, 4, 5, 6)  

// filter: Keep even numbers  
val evens = numbers.filter { it % 2 == 0 }  
println(evens)  // Output: [2, 4, 6]  

// take: First 3 elements  
val firstThree = numbers.take(3)  
println(firstThree)  // Output: [1, 2, 3]  

// drop: Skip first 2 elements  
val afterTwo = numbers.drop(2)  
println(afterTwo)  // Output: [3, 4, 5, 6]  

Checking Conditions: any, all, none

  • any: Returns true if at least one element matches a condition.
  • all: Returns true if all elements match a condition.
  • none: Returns true if no elements match a condition.
val numbers = listOf(2, 4, 6, 8)  

// any: Is there an element > 5?  
println(numbers.any { it > 5 })  // Output: true  

// all: Are all elements even?  
println(numbers.all { it % 2 == 0 })  // Output: true  

// none: Are there no elements < 0?  
println(numbers.none { it < 0 })  // Output: true  

Aggregation: count, sum, maxBy

  • count: Returns the number of elements (or matching a condition).
  • sum: Returns the sum of numeric elements.
  • maxBy/minBy: Returns the element with the maximum/minimum value of a derived property.
data class Person(val name: String, val age: Int)  
val people = listOf(  
    Person("Alice", 30),  
    Person("Bob", 25),  
    Person("Charlie", 35)  
)  

// count: Number of people over 28  
val count = people.count { it.age > 28 }  
println(count)  // Output: 2  

// sum: Sum of ages  
val totalAge = people.sumOf { it.age }  
println(totalAge)  // Output: 90  

// maxBy: Oldest person  
val oldest = people.maxBy { it.age }  
println(oldest?.name)  // Output: Charlie  

Grouping: groupBy, associateBy

  • groupBy: Groups elements by a key derived from each element (returns Map<K, List<T>>).
  • associateBy: Maps elements to a key (returns Map<K, T>, using the last element for duplicate keys).
val words = listOf("apple", "banana", "apricot", "blueberry", "avocado")  

// groupBy: Group words by their first letter  
val grouped = words.groupBy { it.first() }  
println(grouped)  
// Output: {a=[apple, apricot, avocado], b=[banana, blueberry]}  

// associateBy: Map words to their length (last duplicate key wins)  
val wordLengths = words.associateBy { it.length }  
println(wordLengths)  
// Output: {5=apple, 6=banana, 7=avocado} (apricot is length 7 but overwritten by avocado)  

5. Advanced: Sequences for Lazy Evaluation

Most collection operations (e.g., filter, map) are eager: they process the entire collection and return a new collection immediately. For large datasets, this can be inefficient (e.g., filtering a list of 1M elements and then mapping creates an intermediate list).

Sequences (Sequence<T>) provide lazy evaluation: operations are deferred until the result is needed, and elements are processed one at a time. This avoids intermediate collections and reduces memory usage.

Creating Sequences

Use asSequence() to convert a collection to a sequence, or sequenceOf() to create one directly.

// Eager evaluation (creates intermediate lists)  
val eagerResult = listOf(1, 2, 3, 4, 5)  
    .filter { it % 2 == 0 }  // Creates [2, 4]  
    .map { it * 2 }          // Creates [4, 8]  
println(eagerResult)  // Output: [4, 8]  

// Lazy evaluation (no intermediate lists)  
val lazyResult = listOf(1, 2, 3, 4, 5)  
    .asSequence()  
    .filter { it % 2 == 0 }  // Deferred  
    .map { it * 2 }          // Deferred  
    .toList()  // Triggers evaluation  
println(lazyResult)  // Output: [4, 8]  

When to Use Sequences

  • Large collections (e.g., 10k+ elements).
  • Chaining multiple operations (avoids intermediate collections).
  • Short-circuiting (e.g., first() stops processing after finding the first match).
// Short-circuit example: Find first even number > 3  
val numbers = (1..1_000_000).asSequence()  
val result = numbers  
    .filter { it % 2 == 0 }  
    .filter { it > 3 }  
    .first()  // Stops after finding 4  
println(result)  // Output: 4  

6. Best Practices for Using Collections

  1. Prefer Immutable Collections: Use listOf(), setOf(), mapOf() by default. Mutable collections should be confined to local scopes or marked private to avoid unintended side effects.

  2. Choose the Right Implementation:

    • Use ArrayList/hashSetOf for general-purpose, unordered data.
    • Use linkedSetOf/linkedMapOf when insertion order matters.
    • Use sortedSetOf/sortedMapOf for sorted data.
  3. Avoid Unnecessary Copies: Use toList()/toSet() only when converting between mutable/immutable types. Prefer views like subList (for lists) over copying.

  4. Use Sequences for Large Data: For collections with 1k+ elements or complex chains, sequences reduce memory overhead.

  5. Beware of Mutable Collections in Multi-Threading: Mutable collections are not thread-safe by default. Use ConcurrentHashMap or synchronize access if sharing across threads.

7. References


By mastering Kotlin collections, you’ll write cleaner, more efficient code. Start with immutable types, leverage extension functions for common tasks, and use sequences for large datasets. Happy coding! 🚀