Does Kotlin have a default Map?

Have you ever used a default map or default dict before? If you know Python a bit, you probably saw its defaultdict in action at some point. Kotlin also comes with a similar tool which I want to demonstrate in this little article.

You can find the Python defaultdict documentation and some examples here but the basic use case is shown in the following snippet:

from collections import defaultdict d = defaultdict(int) print(d["someKey"]) //0

The defaultdict can also be used with other types and makes sure that you don't get a KeyError when running your code. Instead, it provides a default value for unknown keys, which can be really helpful for grouping and counting algorithms like the following one:

from collections import defaultdict data = [('red', 1), ('blue', 2), ('red', 3), ('blue', 4), ('red', 1), ('blue', 4)] d = defaultdict(set) for k, v in data: d[k].add(v) print(d.items()) # dict_items([('red', {1, 3}), ('blue', {2, 4})])

In this post, I want to show you how Kotlin handles default values of maps and what other alternatives exist.

A simple problem

Let's assume that we want to write a generic function that takes a collection and returns a map containing the elements from the original collection associated to their number of occurrences:

fun <T> countOccurrences(values: Collection<T>): Map<T, Int>

Although there is (of course) a natural and easy functional-style way to do this in Kotlin, we want to consider some iterative approaches first. Let's start with the following one.

Iterative solution without defaults

fun <T> countOccurrences(values: Collection<T>): Map<T, Int> { val countMap = mutableMapOf<T, Int>() for (e in values) { val current = countMap[e] if (current == null) countMap[e] = 1 else countMap[e] = current + 1 } return countMap }

We can use a MutableMap for this algorithm. When iterating all values, we look into the map to see whether it has been seen before. If so, we increment the counter; otherwise, we put an initial count of 1 into the map. A similar, but improved, solution is shown in the next part.

Iterative solution with explicit defaults

//sampleStart countMap.putIfAbsent(e, 0) // getValue , as an alternative to index operator, assures that a non-nullable type is returned countMap[e] = countMap.getValue(e) + 1

As an alternative to the previous solution, we can make use of putIfAbsent to ensure that the element is initialized with 0 so that we can safely increment the counter in the following line. What we do in this case is providing an explicit default value. Another similar tool is getOrDefault :

countMap[e] = countMap.getOrDefault(e, 0) + 1

Providing explicit defaults via putIfAbsent or getOrDefault leads to easy and understandable solutions, but we can do better.

Iterative solution with implicit defaults

Similar to Python's defaultdict , Kotlin comes with a neat extension called MutableMap::withDefault and it looks like this:

fun <K, V> MutableMap<K, V>.withDefault(defaultValue: (key: K) -> V): MutableMap<K, V>

This extension let's us provide an initializer for keys that don't have an associated value in the map. Let's use it in our solution:

fun <T> countOccurrences(values: Collection<T>): Map<T, Int> { val countMap = mutableMapOf<T, Int>().withDefault { 0 } for (e in values) { countMap[e] = countMap.getValue(e) + 1 } return countMap }

It simplifies the code further since we don't need to deal with unknown values in the iteration anymore. Since we're using a default map, we can safely get values from it and increment the counter as we go.

Important note

There's one important thing you need to be aware of when using the withDefault extension, which is part of its documentation:

[...] This implicit default value is used when the original map doesn't contain a value for the key specified, and a value is obtained with [Map.getValue] function [...]

The default value will only be provided if you use Map::getValue , which is not the case when accessing the map with index operators as you can observe here (hit the run button):

fun main(){ val map = mutableMapOf<String, Int>().withDefault { 0 } println(map["hello"]) println(map.getValue("hello")) }

The reason for this is the contract of the Map interface, which says:

Returns the value corresponding to the given [key], or null if such a key is not present in the map.

Since default maps want to fulfill this contract, they cannot return anything but null in the case of non-existent keys, which has been discussed in the Kotlin forum before.

Let's go idiomatic

All right, so saw that Kotlin indeed comes with means of default values in maps, which is fantastic. But, is it idiomatic to write code as we did in the examples above anyway? I think it depends. Most cases can be solved much simpler using Kotlin's outstanding functional APIs which come with built-in grouping and counting functionalities. Nevertheless, it's good to know the alternatives because, in certain situations, you might opt to go with an iterative approach. Let me provide you with a simplified version of our code:

fun <T> countOccurrences(values: Collection<T>): Map<T, Int> = mutableMapOf<T, Int>().withDefault { 0 }.apply { for (e in values) { put(e, getValue(e) + 1) } }

It still uses an explicit for loop but got simplified by the use of apply which allows us to initialize the map in a single statement.

There are dozens of ways to solve the given problem in Kotlin, but probably the simplest one is a functional approach:

fun <T> countOccurrences(values: Collection<T>): Map<T, Int> = values.groupingBy { it }.eachCount()

Conclusion

As shown in this little article, you can solve simple algorithms in many different ways using Kotlin. Similar to Python, you can initialize a map with a default value provider but need to be aware that the regular index operation access won't work the way you might expect. As documented, the getValue function has to be used. Since Kotlin comes with a very useful standard library, you want to consider using it as much as possible. This means that implementing common things like grouping or counting should, in most cases, not be implemented from scratch but rather be done with existing functionalities from the standard library.