By Dmitry Jemerov and Svetlana Isakova

Save 37% off Kotlin in Action with code fccjemerov.

The functional style provides many benefits when it comes to manipulating collections. You can use library functions for most tasks and simplify your code. In this article, we’ll discuss some of the functions in the Kotlin standard library for working with collections. We’ll start with staples like filter and map and the concepts behind them. We’ll cover other useful functions and give you tips about how not to overuse them and write clear and comprehensible code.

Note that none of these functions were invented by the designers of Kotlin. These or similar functions are available for all languages that support lambdas, including C#, Groovy and Scala. If you’re already familiar with these concepts, you can quickly look through the following examples and skip the explanations.


Essentials: filter and map

The filter and map functions form the basis for manipulating collections. Many requests to collections can be expressed with their help.

For each function, we’ll provide one example with numbers and one using the familiar Person class:

 
data class Person(val name: String, val age: Int)

The filter function goes through a collection and selects the elements for which the given lambda returns true:

 
>>> val list = listOf(1, 2, 3, 4)
>>> println(list.filter { it % 2 == 0 })   
[2, 4]

  Only even numbers remain.

The result is a new collection that contains only the elements from the input collection that satisfy the predicate, as illustrated in figure 1.


Figure 1. The filter function selects elements matching given predicate.


If you want to keep only people older than 30, you can use filter:

 
>>> val people = listOf(Person("Alice", 29), Person("Bob", 31)) 
>>> println(people.filter { it.age > 30 }) 
[Person(name=Bob, age=31)]

The filter function can remove unwanted elements from a collection, but it doesn’t change the elements themselves. Transforming elements is where map comes into play.

The map function applies the given function to each element in the collection and collects the results into a new collection. You can transform a list of numbers into a list of their squares, for example:

 
>>> val list = listOf(1, 2, 3, 4) 
>>> println(list.map { it * it }) 
[1, 4, 9, 16]

The result is a new collection that contains the same number of elements, but each element is transformed based on the given predicate (see figure 2).


Figure 2. The map function applies a lambda to all elements in a collection.


If you want to print a list of names, not a list of people, you can transform the list using map:

 
>>> val people = listOf(Person("Alice", 29), Person("Bob", 31)) 
>>> println(people.map { it.name }) 
[Alice, Bob]

Note that this example can be nicely rewritten using member references:

people.map(Person::name)

You can chain several calls like that. For example, let’s print the names of people older than 30:

 
>>> people.filter { it.age > 30 }.map(Person::name) 
[Bob]

Now, let’s say you need the names of the oldest people in the group. You can find the maximum age of the people in the group and return everyone who’s that age. It’s easy to write such code using lambdas:

 
people.filter { it.age == people.maxBy(Person::age).age }

Note that this code repeats the process of finding the maximum age for every person. If there are one hundred people in the collection, the search for the maximum age will be performed one hundred times!

The following solution improves on that and calculates the maximum age once:

 
val maxAge = people.maxBy(Person::age).age 
people.filter { it.age == maxAge }

Don’t repeat a calculation if you don’t need to! Simple-looking code using lambda expressions can sometimes obscure the complexity of the underlying operations. Always keep in mind what’s happening in the code.

You can also apply the filtering and transformation functions to maps:

 
>>> val numbers = mapOf(0 to "zero", 1 to "one") 
>>> println(numbers.mapValues { it.value.toUpperCase() }) 
{0=ZERO, 1=ONE}

Separate functions exist to handle keys and values. filterKeys and mapKeys filter and transform the keys of a map, respectively, whereas filterValues and mapValues filter and transform the corresponding values.


all, any, count, and find: applying a predicate to a collection

Another common task is checking whether all elements in a collection match a certain condition (or, as a variation, whether any elements match). In Kotlin, this is expressed through the all and any functions. The count function checks how many elements satisfy the predicate, and the find function returns the first matching element.

To demonstrate those functions, let’s define the predicate canBeInClub27 to check whether a person is 27 or younger:

 
val canBeInClub27 = { p: Person -> p.age <= 27 }

If you’re interested in whether all the elements satisfy this predicate, you use the all function:

 
>>> val people = listOf(Person("Alice", 27), Person("Bob", 31)) 
>>> println(people.all(canBeInClub27)) 
false

If you need to check whether there’s at least one matching element, use any:

 
>>> println(people.any(canBeInClub27)) 
true

Note that !all (“not all”) with a condition can be replaced by any with a negation of that condition, and vice versa. To make your code easier to understand, you should choose a function that doesn’t require you to put a negation sign before it:

 
>>> val list = listOf(1, 2, 3) 
>>> println(!list.all { it == 3 })   
true 
>>> println(list.any { it != 3 })    
true

❶  The negation ! isn’t noticeable, and it’s better to use “any” in this case.

❷  The condition in the argument has changed to its opposite.

The first check ensures that not all elements are equal to 3. That’s the same as having at least one non-3, which is what you check using any on the second line.

If you want to know how many elements satisfy this predicate, use count:

 
>>> val people = listOf(Person("Alice", 27), Person("Bob", 31)) 
>>> println(people.count(canBeInClub27)) 
1

Using the right function for the job: count vs. size

It’s easy to forget about count and implement it by filtering the collection and getting its size:

 
>>> println(people.filter(canBeInClub27).size) 
1

In this case, an intermediate collection is created to store all the elements that satisfy the predicate. On the other hand, the count method tracks the number of matching elements, not the elements themselves, and is therefore more efficient.

Generally, try to find the most appropriate operation that suits your needs.

To find an element that satisfies the predicate, use the find function:

 
>>> val people = listOf(Person("Alice", 27), Person("Bob", 31)) 
>>> println(people.find(canBeInClub27)) 
Person(name=Alice, age=27)

This returns the first matching element if there are many, or null if nothing satisfies the predicate. A synonym of find is firstOrNull, which you can use if it clarifies the idea for you.


groupBy: converting a list to a map of groups

Imagine that you need to divide all elements into different groups based on some quality. For example, you want to group people of the same age together. It’s convenient to pass this quality directly as a parameter. The groupBy function can do this for you:

 
>>> val people = listOf(Person("Alice", 31), 
...         Person("Bob", 29), Person("Carol", 31)) 
>>> println(people.groupBy { it.age })

The result of this operation is a map from the key by which the elements are grouped (age, in this case) to the groups of elements (persons); see figure 3.


Figure 3. The result of applying the groupBy function


For this example, the output is as follows:

 
{29=[Person(name=Bob, age=29)], 
 31=[Person(name=Alice, age=31), Person(name=Carol, age=31)]}

Each group is stored in a list, and the resulting type is Map<Int, List<Person>>. You can do further modifications with this map, using functions such as mapKeys and mapValues.

As another example, let’s see how to group strings by their first character using member references:

 
>>> val list = listOf("a", "ab", "b") 
>>> println(list.groupBy(String::first)) 
{a=[a, ab], b=[b]}

Note that first here isn’t a member of the String class, it’s an extension. Nevertheless, you can access it as a member reference.


flatMap and flatten: processing elements in nested collections

Now let’s put aside our discussion of people and switch to books. Suppose you have a collection of books, represented by the class Book:

 
class Book(val title: String, val authors: List<String>)

Each book was written by one or more authors. You can compute the set of all the authors in your library:

 
books.flatMap { it.authors }.toSet()

  Set of all authors who wrote books in “books” collection

The flatMap function does two things: at first it transforms (or maps) each element to a collection based on the function given as an argument, and then it combines (or flattens) several lists into one. An example with strings illustrates this concept well (see figure 4):

 
>>> val strings = listOf("abc", "def") 
>>> println(strings.flatMap { it.toList() }) 
[a, b, c, d, e, f]

Figure 4. The result of applying the flatMap function


The toList function on a string converts it into a list of characters. If you use the map function together with toList, you’ll get a list of lists of characters, as shown in the second row in the figure. The flatMap function does the following step as well, and returns one list consisting of all the elements.

Let’s return to the authors:

 
>>> val books = listOf(Book("Thursday Next", listOf("Jasper Fforde")), 
...                    Book("Mort", listOf("Terry Pratchett")), 
...                    Book("Good Omens", listOf("Terry Pratchett", 
...                                              "Neil Gaiman"))) 
>>> println(books.flatMap { it.authors }.toSet()) 
[Jasper Fforde, Terry Pratchett, Neil Gaiman]

Each book can be written by multiple authors, and the book.authors property stores the collection of authors. The flatMap function combines the authors of all the books in a single, flat list. The toSet call removes duplicates from the resulting collection—in this example, Terry Pratchett is listed only once in the output.

You may think of flatMap when you’re stuck with a collection of collections of elements that must be combined into one. Note that if you don’t need to transform anything and need to flatten such a collection, you can use the flatten function: listOfLists.flatten().

We’ve highlighted a few of the collection operation functions in the Kotlin standard library, but there are many more. Our general advice when you write code that works with collections is to think of how the operation could be expressed as a general transformation, and to look for a library function that performs such a transformation. It’s likely that you’ll be able to find one and use it to solve your problem more quickly than with a manual implementation.

That’s it for this article. For more info on Kotlin download the free first chapter of Kotlin in Action and see this Slideshare presentation for more details. Don’t forget to save 37% with code fccjemerov.