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 Pair
s, 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 Pair
s. 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.