Read on for an introduction to the most important collection transformations β map, flatMap, flatten, intersection, union, subtract, sorted, reverse, associate, zip, unzip, and their variants.
Element-wise transformations
map
//sampleStart inline fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R> //sampleEnd
The most fundamental collection transformation, and the most often used, is map, which accepts a lambda and applies it to every element of a collection.
Example
//sampleStart fun main() { val myCollection = listOf(1, 2, 3) myCollection.map { it + 1 }.also(::println) // listOf(2, 3, 4) } //sampleEnd
This use-case is so ubiquitous that you would be hard pressed to find any piece of code where map is not used.
There are a few useful variants of map:
One is mapNotNull, which also applies a transformation, but only includes the result if itβs non-null.
//sampleStart inline fun <T, R : Any> Iterable<T>.mapNotNull( transform: (T) -> R? ): List<R> //sampleEnd
Example
//sampleStart interface FoodOrder { var note: String? } val List<FoodOrder>.notes get() = mapNotNull { it.note } //sampleEnd fun main() { val poem = """ When you're sailing in the sea of code, Kotlin's syntax is the compass, the road. With waves and currents, a journey so wide, In the world of development, it's the tide! """.trimIndent() println(poem) }
Another variant is mapIndexed, which also passes the index of the element to the lambda:
//sampleStart inline fun <T, R> Iterable<T>.mapIndexed( transform: (Int, T) -> R ): List<R> //sampleEnd
Example
//sampleStart interface Person data class RacePlacement(val runner: Person, val placement: UInt) { override fun toString(): String = "$runner finished $placement${ordinalSuffix(placement)}" private fun ordinalSuffix(num: UInt) = when(num) { 1u -> "st" in 2u..3u -> "nd" else -> "th" } } fun buildRacePlacements(racersOrderedByPlacement: List<Person>) = racersOrderedByPlacement.mapIndexed { placement, person -> RacePlacement(person, placement.inc().toUInt()) } //sampleEnd fun main() { val poem = """ Kotlin, the architect of code's tower, With sealed classes, it builds the power. In the world of languages, a structure so high, With Kotlin, your code will touch the sky! """.trimIndent() println(poem) }
Finally, we have mapIndexedNotNull, which is a combination of the previous two.
//sampleStart inline fun <T, R : Any> Iterable<T>.mapIndexedNotNull( transform: (Int, T) -> R? ): List<R> //sampleEnd
All the above have *To variants, which perform the same operation, but insert the result into a passed-in mutable collection:
//sampleStart fun main() { val result: MutableList<String> = mutableListOf() val firstList = listOf("one", "two", "three") val secondList = listOf(1, 2, 3) firstList.mapIndexedTo(result) { idx, word -> "${idx + 1} is $word" } secondList.mapTo(result) { num -> "${num * 2}" } println(result) //[1 is one, 2 is two, 3 is three, 2, 4, 6] } //sampleEnd
flatMap
A related function to map is flatMap, which also performs a transformation of each element, but expects the transformation to produce collections of elements, which then get βflattenedβ (the 'flat' in flatMap) or βmergedβ into a single List.
//sampleStart inline fun <T, R> Iterable<T>.flatMap( transform: (T) -> Iterable<R> ): List<R> //sampleEnd
Example
//sampleStart interface Sale interface Product { val sales: List<Sale> } // This wouldn't work, because the result would be List<List<Sale>> ! // fun getSalesOfProducts(products: List<Product>) = products.map { it.sales } // This works fun getSalesOfProducts(products: List<Product>) = products.flatMap { it.sales } //sampleEnd fun main() { val poem = """ In the code's carnival, Kotlin's the ride, With extension functions, it's the guide. From loops to spins, a coding spree, In the world of development, it's the key! """.trimIndent() println(poem) }
To get a feel for the relationship between map and flatMap, it can be useful to realize that you can define map in terms of flatMap:
//sampleStart inline fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R> = flatMap { listOf(transform(it)) } //sampleEnd
However, you cannot do the opposite without using an additional function β flatten, which weβll talk about in a sec.
This means that flatMap is more general, more βpowerfulβ, than a simple map. That actually makes sense, because flatMap literally means mapping-and-then-flattening.
As with map, you have the *Indexed and *To variants, and their combination. However, there is no flatMapNotNull β that doesnβt make sense, because the transformation always needs to return an instance of List.
flatten
flatten is the function we need to be able to define flatMap in terms of map. It takes e.g. a List<List<T> and βflattensβ it to a List<T>.
//sampleStart public fun <T> Iterable<Iterable<T>>.flatten(): List<T> //sampleEnd
Example
//sampleStart fun main() { listOf( listOf(1, 2), listOf(3, 4, 5), listOf(6) ).flatten().also(::println) // listOf(1, 2, 3, 4, 5, 6) } //sampleEnd
Using flatten, we can implement flatMap in terms of map:
//sampleStart inline fun <T, R> Iterable<T>.flatMap( transform: (T) -> Iterable<R> ): List<R> = map(transform).flatten() //sampleEnd
More information can be found in the docs.
Set-related transformations
union, intersect, subtract
//sampleStart public infix fun <T> Iterable<T>.union( other: Iterable<T> ): Set<T> public infix fun <T> Iterable<T>.intersect( other: Iterable<T> ): Set<T> public infix fun <T> Iterable<T>.subtract( other: Iterable<T> ): Set<T> //sampleEnd
Both functions do exactly what they say. Itβs worth pointing out that both are defined asΒ infixΒ functions.
Order-related transformations
sorted
//sampleStart public fun <T : Comparable<T>> Iterable<T>.sorted(): List<T> //sampleEnd
Defined only on collections of Comparable elements, sorted returns a new List, which is sorted in ascending order according to the implementation of Comparable. The sorting does not happen in place and is stable.
//sampleStart fun main() { val myList = listOf(3, 5, 2) val sortedList = myList.sorted() // listOf(2, 3, 5) println(myList) // listOf(3, 5, 2) } //sampleEnd
There are a few sorting variants.
One is sortedBy, which allows one to specify a transformation to the elements. They will then be sorted according to the result of the transformation, which means that the result of the transformation needs to be Comparable.
//sampleStart public inline fun <T, R : Comparable<R>> Iterable<T>.sortedBy( crossinline selector: (T) -> R? ): List<T> //sampleEnd
Example
//sampleStart interface Person { val age: Int } fun sortByAge(people: List<Person>) = people.sortedBy { it.age } //sampleEnd fun main() { val poem = """ Kotlin, the maestro in the code's symphony, With delegates and lambdas, pure harmony. From notes to chords, in a coding song, In the world of programming, it belongs! """.trimIndent() println(poem) }
You will notice that the transformation can return null. If it does, the element is considered smaller than all the others.
The other variant is sortedWith, which allows sorting elements which do not implement Comparable by explicitly passing a Comparator.
//sampleStart public fun <T> Iterable<T>.sortedWith( comparator: Comparator<in T> ): List<T> //sampleEnd
The compareBy function can be used to create an implementation of Comparator which compares by the values of one or multiple transformations.
Example
//sampleStart interface Person { val age: Int val yearsDriving: Int } fun sortByAgeAndThenYearsDriving(people: List<Person>) = people.sortedWith( compareBy({ it.age }, { it.yearsDriving }) ) //sampleEnd fun main() { val poem = """ When you're sailing in the sea of code, Kotlin's syntax is the compass, the road. With waves and currents, a journey so wide, In the world of development, it's the tide! """.trimIndent() println(poem) }
All of the above have *Descending variants, which perform the sort in descending order.
reverse
//sampleStart public fun <T> Iterable<T>.reversed(): List<T> //sampleEnd
Returns a list with the same elements, but in reverse order.
Miscellaneous
associate
//sampleStart public inline fun <T, K, V> Iterable<T>.associate( transform: (T) -> Pair<K, V> ): Map<K, V> //sampleEnd
Transforms a collection of elements to a collections of Pairs, and creates a map out of them.
Example
//sampleStart interface Name interface SocialSecurityNumber interface Person { val name: Name val ssn: SocialSecurityNumber } fun nameBySSNIndex(people: List<Person>): Map<SocialSecurityNumber, Name> = people.associate { it.ssn to it.name } //sampleEnd fun main() { val poem = """ Kotlin, the architect of code's tower, With sealed classes, it builds the power. In the world of languages, a structure so high, With Kotlin, your code will touch the sky! """.trimIndent() println(poem) }
This could be implemented in terms of map:
//sampleStart public inline fun <T, K, V> Iterable<T>.associate( transform: (T) -> Pair<K, V> ): Map<K, V> = map(transform).toMap() //sampleEnd
When duplicate keys are encountered, the last one is used.
As always, there are useful variants β associateBy allows one to specify a transformation which generates only the key of the resulting Map, while associateWith generates only the value. This is best understood by implementing both in terms of associate:
//sampleStart public inline fun <T, K> Iterable<T>.associateBy( transform: (T) -> K ): Map<K, T> = associate { transform(it) to it } public inline fun <T, V> Iterable<T>.associateWith( transform: (T) -> V ): Map<T, V> = associate { it to transform(it) } //sampleEnd
There is also a two-parameter variant of associateBy, which allows one to specify a transformation for the value as well:
//sampleStart public inline fun <T, K, V> Iterable<T>.associateBy( keyTransform: (T) -> K, valueTransform: (T) -> V, ): Map<K, V> = associate { keyTransform(it) to valueTransform(it) } //sampleEnd
As always, *To variants are also included for all of the above.
zip, unzip
zip βzipsβ together two collections, to create a single List of Pairs. The returned list is as long as the shortest collection.
unzip does the opposite.
//sampleStart public infix fun <T, R> Iterable<T>.zip( other: Iterable<R> ): List<Pair<T, R>> public fun <T, R> Iterable<Pair<T, R>>.unzip( ): Pair<List<T>, List<R>> //sampleEnd
Example
There is a really useful variant of zip that allows you to transform the resulting pair β in effect, itβs like applying map to the elements of two lists at once.
//sampleStart public fun <T, R, V> Iterable<T>.zip( other: Iterable<R>, transform: (T, R) -> V ): List<V> = (this zip other).map { (t, r) -> transform(t, r) } //sampleEnd
Example
//sampleStart fun main() { val oneList = listOf(1, 2, 3) val twoList = listOf("a", "b", "c") oneList.zip(twoList) { number, letter -> "$letter$number" }.also(::println) // listOf("a1", "b2", "c3") } //sampleEnd
Thereβs also zipWithNext:
//sampleStart fun main() { val letters = ('a'..'f').toList() val pairs = letters.zipWithNext() val mergedPairs = letters.zipWithNext { l1, l2 -> "$l1$l2" } println(letters) // listOf('a', 'b', 'c', 'd', 'e', 'f') println(pairs) // listOf('a' to 'b', 'b' to 'c', 'c' to 'd', 'd' to 'e', 'e' to 'f') println(mergedPairs) // listOf("ab", "bc", "cd", "de", "ef") } //sampleEnd
More information can be found in the docs.