1. Overview

When working with List in Kotlin, we often need to perform some operation on each list element. Kotlin provides a powerful set of functions to apply functions to list elements effectively.

In this tutorial, we’ll explore various methods and approaches for this common task.

2. Introduction to the Problem

There can be a couple of scenarios when we’d like to apply a function to list elements. The first is when we want to transform the elements into other values by a function. For example, we may want to convert an Int list to a String list through a toString() function or transform a list of IDs to the corresponding database entities via a database query function. In this scenario, we take each element as the function’s argument, and the function’s return value will be the transformed element.

The other case is calling a function with each list element as the argument, but we don’t care about the result, such as sending an SMS to a list of mobile numbers or outputting each element’s value to the console as log entries.

We’ll discuss these two scenarios in this tutorial and see different techniques to achieve our goals. However, before we explore how to apply functions to elements, let’s prepare some example functions:

fun String.reverseCase() = map { c -> if (c.isLowerCase()) c.uppercase() else c.lowercase() }.joinToString(separator = "")

fun String.printCaseReversed() = println("$this -> ${reverseCase()}")

As we can see, we’ve created two functions extending String. reverseCase() reverses each character’s case in the input string and returns the result. While the printCaseReversed() function calls reverseCase() internally and prints the result without returning a value.

Next, let’s initialize a list:

val myList = listOf("a a a", "B B B", "c C c", "D d D", "e E E", "F F f")

If we perform the reverseCase() function on String elements in this list, each lowercase character becomes uppercase and vice versa. So, we’ll have the following list:

val expected = listOf("A A A", "b b b", "C c C", "d D d", "E e e", "f f F")

For simplicity, we’ll use unit test assertions to verify whether each solution produces the expected result.

3. Using the map() Function

Using the map() function is a common approach when talking about transforming a list. The map() function transforms every element A into a new value B during the iteration.

Next, let’s use the map() function to apply reverseCase() on each element in myList:

val result = myList.map { it.reverseCase() }
assertEquals(expected, result)

As we can see, the map() function returns a new list object (result) carrying the transformed elements.

4. Using the replaceAll() Function on a MutableList

We’ve learned how to use map() to transform a list by applying a function on each element. In the example, our myList is a read-only list, so we cannot modify its elements by reassigning them.

However, when we work with Kotlin’s MutableList, we may want to perform in-place transformation. Then, the replaceAll() function can help us to achieve it:

val mutableList = myList.toMutableList()
assertEquals(myList, mutableList)

mutableList.replaceAll { it.reverseCase() }
assertEquals(expected, mutableList)

As the test above shows, replaceAll()‘s usage is pretty similar to the map() function. After calling mutableList.replaceAll(), mutableList has been in-place transformed.

5. Using the forEach() Function

So far, we discussed transforming a list by applying a function to the elements. In other words, map() and replaceAll() take the function’s return values as the transformed elements. Next, let’s look at another scenario in which we call a function as performing an action and don’t care about the function’s return value. In this scenario, the original list won’t be changed.

The most straightforward idea to achieve that is looping through the elements and calling the required function. However, Kotlin offers better ways to do this kind of task. For example, we can use the forEach() function to act on each element in the list:

myList.forEach { it.printCaseReversed() } // output the result of each element

After executing this line, we can see the output:

a a a -> A A A
B B B -> b b b
c C c -> C c C
D d D -> d D d
e E E -> E e e
F F f -> f f F

Another use case of forEach() is from one collection to fill the other collection:

val result = mutableListOf<String>()
myList.forEach { result += it.reverseCase() }
assertEquals(expected, result)

The usage of the forEach() function is straightforward. However, it’s worth noting that this function returns Unit:

public inline fun <T> Iterable<T>.forEach(action: (T) -> Unit): Unit {
    for (element in this) action(element)
}

This means after calling forEach(), we cannot chain other functions to process the list further. So, it’s a terminal function. This can be inconvenient in some cases.

Next, let’s see how to overcome this inconvenience.

6. Using the onEach() Function

Next to the forEach() function, Kotlin provides the onEach() function. Let’s first look at its implementation:

public inline fun <T, C : Iterable<T>> C.onEach(action: (T) -> Unit): C {
    return apply { for (element in this) action(element) }
}

If we compare the implementations of forEach() and onEach(), we can see that the significant difference is onEach() wraps the for-loop in an apply() call. The receiver.apply{ … } call performs the operations in the {…} block and returns the receiver object. In the onEach() case, the receiver is the list itself. Therefore, list.onEach() performs an action on each element in the list, which is the same as what list.forEach() does and then returns the list object:

val result = myList.onEach { it.printCaseReversed() }
assertSame(myList, result)

As the test shows, after calling onEach(), the myList object is returned. Also, looking at the output, we can tell that the printCaseReversed() function has been applied to each element:

a a a -> A A A
B B B -> b b b
c C c -> C c C
D d D -> d D d
e E E -> E e e
F F f -> f f F

Since list.onEach() returns list, we can chain other functions to perform further processing after calling onEach(), for example:

val resultList = myList.onEach { it.printCaseReversed() }
  .map { it.uppercase().replace(" ", ", ") }
assertEquals(listOf("A, A, A", "B, B, B", "C, C, C", "D, D, D", "E, E, E", "F, F, F"), resultList)

7. Conclusion

In this article, we first discussed two scenarios of applying a function to a list’s elements. Then, we explored various approaches for this task.

As always, the complete source code for the examples is available over on GitHub.

Comments are closed on this article!