When dealing with Maps in Kotlin, sometimes we’re only interested in entries for which the value is not null. Although the Kotlin Standard Library contains a filterValues function that seems to be appropriate, this function does not do any type conversions, resulting in a Map which won’t contain null values, but is still a Map with values of a nullable type according to the compiler. There is a feature request for the JetBrains team to add this functionality, but for now it has not been implemented (yet?).

The problem

Imagine we have the following Map of people associated with their age (if known):

val ages: Map<String, Int?> = mapOf(
    "Alice" to 25,
    "Bob" to null,
    "Charlie" to 30
)

As you can see, the value type of the Map is Int?, because we don’t know Bob’s age.

If we would like to filter this map to retain only the people with a known age, our first instinct would be to use the filterValues function. It would look like this:

val knownAges = ages.filterValues { it != null }

Although the resulting knownAges Map would indeed no longer contain the null entry for Bob, the type of the Map would still be inferred as Map<String, Int?>. This means that the compiler will not be able to leverage the fact that the Map will only contain non-null values.

The solution(s)

There are several ways we can fix this. Perhaps the easiest way would be to just cast the Map to the 'correct' type:

val knownAges = ages.filterValues { it != null } as Map<String, Int>

Unfortunately, the Kotlin compiler will complain about an Unchecked cast: Map<String, Int?> to Map<String, Int>. Even though the compiler is right, we know that the cast will be safe in this case. Thus, we could ignore the warning (which I personally don’t like), or suppress it (which makes the fact that we ignored it more explicit). To prevent littering the code with suppressions, we could create an extension function on the Map class:

@Suppress("UNCHECKED_CAST")
fun <K, V> Map<K, V?>.filterNotNullValues(): Map<K, V> =
        filterValues { it != null } as Map<K, V>

To use the function, we simply call it like this:

val knownAges = ages.filterNotNullValues() // The resulting type of knownAges will be: Map<String, Int>

If you’re not a fan of unchecked casts, or if you consider yourself a purist, or maybe if your static code analyzers don’t allow suppression of unchecked cast warnings, there are some alternatives:

Alternative 1: using mapNotNull

fun <K, V> Map<K, V?>.filterNotNullValues(): Map<K, V> =
        mapNotNull { (key, value) -> value?.let { key to it } }.toMap()

The mapNotNull function iterates the entries of a Map, performs the passed transforming operation on the entry, and if the result is non-null, adds it to the resulting list. The transforming operation in this case consists of using the let function to create a key-value Pair<String, Int> of the entry, but only if value is not null. The output of the mapNotNull function is a List<Pair<String, Int>>, which we convert back to a Map with the toMap function.

Pros:

  • No compiler warnings;

  • No explicit cast required.

Cons:

  • Not intuitive to read;

  • Requires an intermediate List, which consumes more memory (negligible for small Maps but might become an issue on larger Maps).

Alternative 2: manually constructing a new Map

fun <K, V> Map<K, V?>.filterNotNullValues(): Map<K, V> =
        mutableMapOf<K, V>().apply { for ((k, v) in this@filterNotNullValues) if (v != null) put(k, v) }

We create a new MutableMap<K, V>, and using the apply function we manually put all the entries for which the value is not null.

Pros:

  • No compiler warnings;

  • No explicit cast required;

  • No intermediate Collection required, so less memory-intensive than Alternative 1.

Cons:

  • 'Feels' like a dirty solution, because we explicitly use a MutableMap (while immutability is such a nice topic), and a for loop (which might depend on your preference);

  • Also less intuitive, e.g. because we’re using labels.

Alternative 3: using a Map builder (experimental)

@ExperimentalStdlibApi
fun <K, V> Map<K, V?>.filterNotNullValues(): Map<K, V> =
        buildMap { for ((k, v) in this@filterNotNullValues) if (v != null) put(k, v) }

Kotlin 1.3 introduced an experimental API for building a Map by specifying a builderAction which populates the Map. The returned Map is read-only. This solution looks a lot like the previous one, but eliminates the use of a MutableMap. Instead, it uses a Builder-style pattern. There is an intermediate Map builder inside the builderAction that’s populated with entries (in a way similar to the previous solution), but ultimately it returns an immutable Map.

Pros:

  • No compiler warnings;

  • No explicit cast required;

  • Like the previous solution, but without the explicit use of a MutableMap.

Cons:

  • Still using a for loop;

  • The Map builder is experimental, so each function calling this function must be annotated with @ExperimentalStdlibApi until the feature is no longer experimental. Of course, because it’s an experimental feature, it might also be subject to change.

Conclusion

Like any software problem, there are a multitude of possible solutions, each with their own (dis)advantages. Of course there are other ways of solving this problem, but this post only intends to guide those seeking a solution.

My personal preference is to use an extension function with the first proposed solution: an explicit cast with a suppression. The suppression is acceptable to me because at that point I actually do know better than the compiler that the Map values will never be null. Furthermore, the suppression is localized to a single one-line extension method, which can be reused throughout the codebase.

In the end, just choose the solution you feel most comfortable with!

Compiled and tested with Kotlin version 1.4.10.

shadow-left