Introduction to Kotlin Collections and Their Construction
Kotlin Collections Overview
Kotlin's kotlin.collections package provides three primary collection types: List (ordered), Set (unordered, unique elements), and Map (key-value pairs).
A collection defined with val is immutable in its reference, not necessarily its content. You can modify a mutable collection, but you cannot reassign the variable to a new collection instance.
val items = mutableListOf("apple", "banana", "cherry")
items.add("date") // This is allowed.
// items = mutableListOf("elderberry", "fig") // Compilation error: Val cannot be reassigned.
The Collection Interface
Collection<T> is the root of the collection hierarchy, representing read-only behaviors like checking size and membership. It inherits from Iterable<T>. For more specific use cases, use its inheritors: List and Set.
fun displayItems(elements: Collection<String>) {
for (element in elements) print("$element ")
println()
}
fun main() {
val fruitList = listOf("apple", "banana", "apple")
displayItems(fruitList)
val fruitSet = setOf("apple", "banana", "cherry")
displayItems(fruitSet)
}
MutableCollection extends Collection with write operations like add and remove.
fun List<String>.extractShortWords(result: MutableList<String>, maxLen: Int) {
this.filterTo(result) { it.length <= maxLen }
val determiners = setOf("a", "A", "an", "An", "the", "The")
result -= determiners
}
fun main() {
val sentence = "A long time ago in a galaxy far far away".split(" ")
val shortWords = mutableListOf<String>()
sentence.extractShortWords(shortWords, 3)
println(shortWords) // Output: [ago, in, far, far]
}
List
List<T> stores elements in a defined sequence and allows indexed access starting from 0.
val colors = listOf("red", "green", "blue", "yellow")
println("Element count: ${colors.size}")
println("Third element: ${colors.get(2)}")
println("Fourth element: ${colors[3]}")
println("Index of 'green': ${colors.indexOf("green")}")
Lists can contain duplicate elements, including null. Two lists are equal if they have the same size and identical elements in the same order.
data class User(val name: String, var age: Int)
val alice = User("Alice", 30)
val group1 = listOf<User>(User("Bob", 25), alice, alice)
val group2 = listOf<User>(User("Bob", 25), User("Alice", 30), alice)
println(group1 == group2) // true
alice.age = 31
println(group1 == group2) // false
MutableList supports write operations.
val values = mutableListOf(10, 20, 30, 40)
values.add(50)
values.removeAt(1)
values[0] = 5
values.shuffle()
println(values)
The default List implementation in Kotlin is ArrayList.
Set
Set<T> stores unique elements; order is generally undefined. A Set can contain at most one null. Two sets are equal if they have the same size and for every element in one set, there is an equal element in the other.
val digits = setOf(5, 6, 7, 8)
println("Set size: ${digits.size}")
if (digits.contains(5)) println("5 is present")
val reversedDigits = setOf(8, 7, 6, 5)
println("Sets are equal: ${digits == reversedDigits}") // true
MutableSet adds write operations. The default implementation, LinkedHashSet, preserves insertion order.
val firstSet = setOf(1, 2, 3, 4) // LinkedHashSet
val secondSet = setOf(4, 3, 2, 1)
println(firstSet.first() == secondSet.first()) // false
println(firstSet.first() == secondSet.last()) // true
HashSet does not guarantee order but uses less memory.
Map
Map<K, V> stores key-value pairs (entries). Keys are unique, but different keys can map to the same value.
val dataMap = mapOf("alpha" to 10, "beta" to 20, "gamma" to 30, "delta" to 10)
println("Keys: ${dataMap.keys}")
println("Values: ${dataMap.values}")
if ("beta" in dataMap) println("Value for 'beta': ${dataMap["beta"]}")
if (10 in dataMap.values) println("The value 10 exists in the map")
MutableMap supports adding or updating entries.
val mutableData = mutableMapOf("first" to 1, "second" to 2)
mutableData.put("third", 3)
mutableData["first"] = 100
println(mutableData)
The default Map implementation is LinkedHashMap. HashMap does not guarentee order.
Constructing Collections
From Elements
The most common way is using library functions: listOf<T>(), setOf<T>(), mutableListOf<T>(), mutableSetOf<T>(). For empty collections, specify the type explicit.
val animalSet = setOf("cat", "dog", "bird")
val emptyMutableSet = mutableSetOf<String>()
For maps, use mapOf() and mutableMapOf(). Pairs are often created with the to infix function.
val countryCodes = mapOf("US" to 1, "UK" to 44, "DE" to 49)
Note: to creates a short-lived Pair. For better performance with mutable maps, consider alternative initialization.
val betterMap = mutableMapOf<String, Int>().apply {
this["one"] = 1
this["two"] = 2
}
Empty Collections
Use emptyList(), emptySet(), emptyMap(). Specify the element type.
val nothingHere = emptyList<Double>()
List Initializer Function
List has a constructor that takes a size and an initializer function.
val squares = List(5) { index -> index * index }
println(squares) // [0, 1, 4, 9, 16]
Concrete Type Constructors
You can use constructors of specific implementations like ArrayList or HashSet.
val linkedList = LinkedList<String>(listOf("x", "y", "z"))
val sizedHashSet = HashSet<Int>(64)
Copying Collections
Copy operations like toList(), toMutableList(), toSet() create shallow copies. Changes to the original collection's elements affect all copies, but adding/removing elements does not.
val original = mutableListOf(100, 200, 300)
val mutableCopy = original.toMutableList()
val readOnlyCopy = original.toList()
original.add(400)
println("Mutable copy size: ${mutableCopy.size}") // 3
// readOnlyCopy.add(400) // Compilation error
println("Read-only copy size: ${readOnlyCopy.size}") // 3
These functoins can also convert between collection types.
val numberList = mutableListOf(7, 8, 9)
val numberSet = numberList.toMutableSet()
numberSet.add(9) // No effect, 9 already exists
numberSet.add(10)
println(numberSet) // [7, 8, 9, 10]
Assigning an existing collection to a new variable creates a reference, not a copy.
val source = mutableListOf("a", "b")
val reference = source
reference.add("c")
println("Source size: ${source.size}") // 3
Initialization can be used to restrict mutability.
val mutableSource = mutableListOf(1.0, 2.0)
val readOnlyView: List<Double> = mutableSource
// readOnlyView.add(3.0) // Compilation error
mutableSource.add(3.0)
println(readOnlyView) // [1.0, 2.0, 3.0]
Invoking Functions on Other Collections
New collections can be created as results of operations like filtering or mapping.
val words = listOf("hello", "world", "kotlin", "code")
val longWords = words.filter { it.length > 4 }
println(longWords) // [hello, world, kotlin]
val nums = setOf(1, 2, 3)
println(nums.map { it * 10 }) // [10, 20, 30]
println(nums.mapIndexed { i, v -> v * i }) // [0, 2, 6]
val items = listOf("book", "pen", "laptop")
println(items.associateWith { it.length }) // {book=4, pen=3, laptop=6}