Inside Kotlin Collections. Part 3

by Andrew Khrystian,
Software Engineer Android @Runtastic

In the two previous articles we talked about collections structure and all the possible ways to create them. Now I am going to show you the coolest thing in Kotlin collections  –  the functional approach Kotlin provides on top of collections.

This approach is very similar to Java 8 collections and Rx usage. So the idea is that you have some stream of data, and using some functions you can receive some required state of the stream. But if we compare it to Rx, there are no subscribers, observers, etc. Everything is running consistently. Let’s check all important functions that we have in the kotlin.collection framework and compare it to an implementation to Java.

Iterating

The most common operation when you work with collections is iteration through collection items

for (int i = 0; i < friends.size(); i++) {
   Friend friend = friends.get(i);
   friend.setId(i);
}

For example, you need to set id for the list item, or any if don’t need an index, you can easily iterate collection using for each:

for (Friend friend: friends) {
   friend.setBirthDay(LocalDate.now());
}

In Kotlin all those loops are unnecessary. Let’s use the functional approach:

friends.forEach {
    it.birthDay = LocalDate.now()
}

and of course when you need indexes, there is a function with indexes:

friends.forEachIndexed {
    index, friend -> friend.id = index
}

There is a really simple realization inside this function:

public inline fun <T> Iterable<T>.forEachIndexed(action: (index: Int, T) -> Unit): Unit {
    var index = 0
    for (item in this) action(index++, item)
}

It just invokes lambda with receiver with iterary incrementing index and particular items.

Now it could seem that there is not much advantage to using functions instead of loops, but let’s go further.

Filtering

Let’s continue with common java examples. Usually when we iterate some collections we need to identify and update some elements. Here is an simple java code example of how we always did that before. We have a list of elements:

friends = mutableListOf<Friend>(Friend("Andrii", "man", 1),
        Friend("Yasya", "women", 2),
        Friend("Alex", "man", 3))

for some reason we need to update the name for item with id == 3. Java implementation:

for(int i = 0; i < friends.size(); i++) {
   Friend friend = friends.get(i);
   if(friend.getId() == 3) {
      friend.setName("Lyosha");
   }
}

Yes, of course it can be simpler for each

for (Friend friend : friends) {
   if (friend.getId() == 3) {
      friend.setName("Lyosha");
   }
}

Yes, that’s better. And now the Kotlin example:

friends.filter { it.id == 3 }.forEach { it.name = "Lyosha" }

One row, without ifs and loops. Let’s check how the filter function works inside

public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
    return filterTo(ArrayList<T>(), predicate)
}
public inline fun <T, C : MutableCollection<in T>> Iterable<T>.filterTo(destination: C, predicate: (T) -> Boolean): C {
    for (element in this) if (predicate(element)) destination.add(element)
    return destination
}

The first function calls the second one. In the second function there is a loop and the if check. So there is no magic. But this realization is more understandable and attractive, because it uses lambdas and generics to make our implementation and support better.

One more important thing that we have to keep in mind is that the filter function doesn’t change the original collection. It just returns a new one.

The filter function has a lot of different options that can improve your code style:

  1. filterNot(Predicate: Item -> Boolean) → It’s the same as the filter but it returns items which do not meet the predicate condition.
  2. filterIndexed(Predicate: (Int, Item) -> Boolean) → Here an index of collection items was added.
  3. filterIsInstance<>() -> It checks the type of collection elements and returns the new collection.
  4. filterNotNull() -> It guarantees that you’ll receive a collection with non-null elements.
  5. filterKeys() and filterValues() -> For maps
  6. And of course all functions with the “to” suffix are also public (e.g filterTo, filterNotTo, etc). Using these functions you can define the collection object that will hold the filtering result.
friends.filterTo(searchResults, { it.id == 3})

Sorting

Sorting is also a common operation when we work with collections. In an ideal world for Android development it should be handled on the backend side, but sometimes we don’t have backend for some data types and we need to do it locally.

There is a task to sort friends by name alphabetically. So how we can implement it using java:

Collections.sort(friends, new Comparator&lt;Friend&gt;() {
   @Override
   public int compare(Friend friend, Friend t1) {
      return friend.getName().compareTo(t1.getName());
   }
});

or obviously you can compare it manually or implement Comparable for the model.

And here comes Kotlin:

friends.sortBy { it.name }

Looks really simple, but let’s check the source code:

public inline fun &lt;T, R : Comparable&lt;R&gt;&gt; MutableList&lt;T&gt;.sortBy(crossinline selector: (T) -&gt; R?): Unit {
    if (size &gt; 1) sortWith(compareBy(selector))
}
@kotlin.internal.InlineOnly
public inline fun &lt;T&gt; compareBy(crossinline selector: (T) -&gt; Comparable&lt;*&gt;?): Comparator&lt;T&gt; =
        Comparator { a, b -&gt; compareValuesBy(a, b, selector) }
@kotlin.jvm.JvmVersion
public fun &lt;T&gt; MutableList&lt;T&gt;.sortWith(comparator: Comparator&lt;in T&gt;): Unit {
    if (size &gt; 1) java.util.Collections.sort(this, comparator)
}

So actually it works the same as java. The second function returns Comparator and compares two collection items, and function sortWith() just uses compare() method from java.utils.Collections. And this function changes the original collection.

Notice that this function can be used only with MutableLists.

In my opinion, this function is useless for the functional approach. What if we need to filter collection and then sort the result? The function sortedBy() can help us solve this:

friends.filter { it.gender == "man" }.sortedBy {  it.name }

And here is java implementation

void test(List&lt;Friend&gt; friends) {
   List&lt;Friend&gt; friendsSorted = new ArrayList&lt;&gt;();
   for (MainActivity.Friend friend : friends) {
      if (friend.getGender().equals("man")) {
         friendsSorted.add(friend);
      }
   }
   Collections.sort(friendsSorted, new Comparator&lt;Friend&gt;() {
      @Override
      public int compare(Friend friend, Friend t1) {
         return friend.getName().compareTo(t1.getName());
      }
   });
}

Huge and ugly. Of course you can use Java 8 features and lambdas to simplify it. And it’ll make this code look simpler.

As for the filtering functions, sortedBy doesn’t change the original collection, and it creates a new collection instead.

The function sortedBy() works almost the same as sortBy() but it’s more complicated. It can be called on all children of iterable and it returns the List. This function is based on the sortedWith() function:

public fun &lt;T&gt; Iterable&lt;T&gt;.sortedWith(comparator: Comparator&lt;in T&gt;): List&lt;T&gt; {
    if (this is Collection) {
       if (size &lt;= 1) return this.toList()
       @Suppress("UNCHECKED_CAST")
       return (toTypedArray&lt;Any?&gt;() as Array&lt;T&gt;).apply { sortWith(comparator) }.asList()
    }
    return toMutableList().apply { sortWith(comparator) }
}

And afterwards it calls sortWith() anyway.

There are also a few more methods that can be used for sorting your collections:

  1. sortByDescending(Predicate) and sortedByDescending(Predicate) just change an order
  2. sortedWith(Comparator) and sortWith(Comparator) are public and can be used with custom Comparator
  3. sorted() can be used when your data model implements Comparable.

Grouping

What if we need to group some big list by some condition? Let’s imagine we have one list of friends:

friends = mutableListOf(Friend("Andrey", "man", 32, "Linz"),
        Friend("Valik", "man", 32, "Dnipro"),
        Friend("Lexa", "man", 31, "Philadelphia"),
        Friend("Tolik", "man", 32, "Dnipro"),
        Friend("Dimon", "man", 32, "Kharkiv"),
        Friend("Nikitos", "man", 32, "Kyiv"),
        Friend("Igor", "man", 32, "Lviv"),
        Friend("Alex", "man", 32, "Linz"),
        Friend("Serega", "man", 32, "Stockholm"),
        Friend("Michael", "man", 32, "New York"),
        Friend("Slavik", "man", 32, "Novomoskosvsk"))

And we need to group them by city and show each list in a different tab, for example. We can do that using java:

void groupJava(List&lt;Friend&gt; friends) {
   Map&lt;String, List&lt;Friend&gt;&gt; friendsCache = new HashMap&lt;&gt;();
   for (Friend friend: friends) {
      String city = friend.getCity();
      if (!friendsCache.keySet().contains(city)) {
         List&lt;Friend&gt; oneCityFriends = new ArrayList&lt;&gt;();
         oneCityFriends.add(friend);
         friendsCache.put(city, oneCityFriends);
      } else {
         friendsCache.get(city).add(friend);
      }
   }
}

And what about Kotlin? That is much easier:

val friendsCache: Map&lt;String, List&lt;Friend&gt;&gt; = friends.groupBy { it.city }

And if you need to sort by city, you can just call toSortedMap()

Let’s check what is going on inside:

public inline fun &lt;T, K, M : MutableMap&lt;in K, MutableList&lt;T&gt;&gt;&gt; Iterable&lt;T&gt;.groupByTo(destination: M, keySelector: (T) -&gt; K): M {
    for (element in this) {
        val key = keySelector(element)
        val list = destination.getOrPut(key) { ArrayList&lt;T&gt;() }
        list.add(element)
    }
    return destination
}

Almost the same 🙂 There is the additional function getOrPut() to work with lists in cache.

Blocking operations

In our case, this function returns some non-iterable objects that contain information about this collection.

Sometimes you need to analyze your collection to check if it contains an element.

any()

For example, you need to know if the collection contains an element with an exact id. In java you have to iterate through the collection and return true if your expression passed at least once. In Kotlin it’s much easier:

friends.any { it.id == 31 }

The result will be true if at least one element has id equal “31”. I would say any is an upgraded contains() from Java.

all()

In case you need to check all elements:

friends.all { it.city == "Dnipro" }

none()

Also, there is a better emptiness check for all iterables:

friends.none()

Basically it checks if the iterable is a collection and returns isEmpty(). If not, it just returns iterator().hasNext().

public fun &lt;T&gt; Iterable&lt;T&gt;.none(): Boolean {
    if (this is Collection) return isEmpty()
    return !iterator().hasNext()
}

min() and max()

Sometimes you also need to find a min or max element in your collection. And here it would be good to show a java example:

Friend maxValueById = friends.stream().max(Comparator.comparing(v -&gt; v.getId)).get();

In java 8 it looks quite good. And what about Kotlin?

val friend = friends.maxBy { it.id }

Awesome.

maxBy() and minBy() can be used for collections that contain Kotlin data classes or java models that implement Comparable.

And if your model is not Kotlin data class and does not implement Comparable, there is also a good solution. Just minWith(Comparable) or maxWith(Comparable).

For numeric types, strings, you can just use max() or min().

find()

This is a common operation with collections. It helps find an element by giving the expression:

val friend = friends.filter { it.city == "Dnipro" }
                    .find { it.name == "Lexa" }

Find() returns you the first found element. And obviously there is a function findLast() to return the last found element.

first(), firstOrNull(), last(), lastOrNull() and component1–5() return exact elements from the collection.

Mapping and Converting Operations

Using these functions you can map or convert your collection to another collection or any other object. The most common operation for converting collections are map() and flatMap(). You might know these functions if you have ever used RxJava or Java 8 streams.

map()

This function just maps one collection to another. For example, you need to get a list of names from the friends list:

val names = friends.map { it.name }

This function just uses mapTo() and adds the result of the delegate to the new collection. Of course mapTo() is also public and can be used in your code.

public inline fun &lt;T, R, C : MutableCollection&lt;in R&gt;&gt; Iterable&lt;T&gt;.mapTo(destination: C, transform: (T) -&gt; R): C {
    for (item in this)
        destination.add(transform(item))
    return destination
}

flatMap()

This function is similar to map with only one big difference. It requires an iterable. Let’s imagine that we have a list of users and each user has some set with skills.

friends = mutableListOf(Friend("Andrew", "man", 32, "Linz",      setOf("Android", "Kotlin", "Java")),
        Friend("Yasya", "man", 32, "Linz", setOf("Flutter", "Java", "Android")),
        Friend("Tolik", "man", 32, "Dnipro", setOf("Python", "Ruby", "PHP")))

And now we need to put all skills into the one List<String>. With Kotlin flatMap() it’s really easy to do:

val skills: List&lt;String&gt; = friends.flatMap { it.set }

Inside it uses public function mapTo() that iterates and adds each item to a new ArrayList

public inline fun &lt;T, R, C : MutableCollection&lt;in R&gt;&gt; Iterable&lt;T&gt;.flatMapTo(destination: C, transform: (T) -&gt; Iterable&lt;R&gt;): C {
    for (element in this) {
        val list = transform(element)
        destination.addAll(list)
    }
    return destination
}

New functions

Kotlin is a fast-growing language, so obviously we have new functions since the latest language version was released.

In Kotlin 1.1 the following new functions were released:

groupingBy()  –  this function returns the grouping object. This object contains an iterator through collection and the keyOf() method that returns the specific key for the collection item.

I’m sure that it sounds unclear, but let me show you an example:

friends = mutableListOf(Friend("Andrey", "man", 32, "Linz"),
        Friend("Valik", "man", 32, "Dnipro"),
        Friend("Lexa", "man", 31, "Philadelphia"),
        Friend("Tolik", "man", 32, "Dnipro"),
        Friend("Dimon", "man", 32, "Kharkiv"))

Here we have the MutableList as before. Let’s group it by city:

val cityGrouping= friends.groupingBy { it.city }i

In debugger we can check the sourceIterator() function

As we can see, it contains our MutableList, and obviously we can use keyOf() method. We can ask which city the fourth user is from in the list:

So you can use different kinds of groupings and receive keys by values. A few more useful functions were released with grouping.

getOrDefault()

Function that can be used with maps. You have to define key and non null default value for cases where there is no value for the key

friends.groupBy { it.city }.getOrDefault("Linz",  listOf(Friend("Valik", "man", 32, "Dnipro")))

eachCountTo()

This function returns a map that contains a count of list items for each key.

onEach()

This function can be used with iterables and maps, and it takes action as a parameter. It changes all elements using the given action:

friends.onEach { it.city = "New York" }

All your items will have a city field equal to “New York”. Here is the source code:

@SinceKotlin("1.1")
public inline fun &lt;T, C : Iterable&lt;T&gt;&gt; C.onEach(action: (T) -&gt; Unit): C {
    return apply { for (element in this) action(element) }
}

As you can see, it changes the existing collection.

Since Kotlin 1.2 we have the following functions:

chunked()

This function can be used for iterables and it takes Int as the argument. Here the argument is chunk size. Chunked() returns a list of chunks that have this special size. Here is an example:

We are splitting collection for chunks, and chunks have size 2. If the original collection does not have enough elements, the last chunk can be smaller (as in the example above)

Here is the source code:

@SinceKotlin("1.2")
public fun &lt;T&gt; Iterable&lt;T&gt;.chunked(size: Int): List&lt;List&lt;T&gt;&gt; {
    return windowed(size, size, partialWindows = true)
}

chunked() calls windowed() function. This function is also new and I show how it works in the next paragraph. Chunked() has one more important overloading function. This function takes size and transforms lambda. You can use this lambda to setup the chunk object. So chunked() can return a list of some objects

windowed()

This function also provides a list of chunks, but here we can also define step, and partialWindows boolean

Step  defines what would be the number of elements to move the window forward; it can’t be ≤ 0. When you set the step less than the chunk size, you will have duplicates in each chunk, so the step should be the same or bigger than chunk size. If partialWindows is false, windowed ignores the last chunk if its size is not equal to the defined chunk size.

shuffle()

Obviously this function can shuffle your elements in the collection. The important point is that it can be only MutableList. Implementation of this function is simple  –  it just invokes java.util.Collections.shuffle(this), the same as in java there is one more implementation that takes java.util.Random. Remember: this functions changes the original MutableList

shuffled()

If you need to shuffle other iterables, you have to use shuffled(). But actually shuffled() just uses shuffle() function. It just converts iterable to a mutable list, and uses the same logic as described above.

@kotlin.jvm.JvmVersion
@SinceKotlin("1.2")
public fun &lt;T&gt; Iterable&lt;T&gt;.shuffled(): List&lt;T&gt; = toMutableList().apply { shuffle() }

No magic.

As you can see, in Kotlin there are a lot of different functions and instruments to deal with collections. Using all of them can make your code clearer and easier to understand. And as described above, the source code and logic looks really simple and smart. Hope you enjoyed my articles about Kotlin Collection framework. And God save Kotlin! 🙂

***

RATE THIS ARTICLE NOW

Runtastic Tech Team We are made up of all the tech departments at Runtastic like iOS, Android, Web, Infrastructure, DataEngineering, etc. We’re eager to tell you how we work and what we have learned along the way. View all posts by Runtastic Tech Team »

Leave a Reply