1. Overview

In Kotlin, we can access a value in a Map using this syntax: map[key]. But it returns a nullable value. So it can be inconvenient when we’re sure the value is not null.

So, in this tutorial, we’ll first talk about what “map[key]” is and how it works. Then, let’s explore how to get a not-null value from a Map.

2. Introduction to the Problem

First of all, let’s look at a Map example:

val inputMap = mapOf(
  1 to "Macbook pro, MacOS",
  2 to "Dell Laptop, Windows10",
  3 to "HP Laptop, Archlinux",
  4 to "Thinkpad Laptop, Ubuntu Linux",
)

We have initialized a Map<Int, String> above. It stores some laptop data. Now, let’s create a simple test to verify that key 3 in the map associates with the value “HP Laptop …“:

val result = inputMap[3]
assertTrue { result!!.startsWith("HP Laptop") }

As the test shows, we can access the value from a key using map[key]. “[]” is Kotlin’s built-in get operator:

public operator fun get(key: K): V?

Here, it does the same as calling inputMap.get(3). But the map[key] style looks natural and improves code readability.

In our test above, we verify if the string value with key=3 starts with “HP …“. The test passes if we run it. However, we have to use !! after the result variable. This is because the Map‘s get(key) function returns a nullable value. When we try to get a value with a nonexistent key, map[not-exist-Key] returns null, for example:

val nullResult = inputMap[42]
assertNull(nullResult)

Null safety is one of the most significant features of Kotlin. It brings many advantages, such as reducing NullPointerExceptions, making code more readable, and so on. But after we get a value from a map using the get operator, we must put !! after the value, as the example above shows. It can be inconvenient, especially when we know the value won’t be null.

So next, let’s see if we can get a not-null value from a map in Kotlin. For simplicity, we’ll use unit test assertions to verify if the value is expected.

3. Using the getValue() Function

Kotlin’s Map provides the getValue() function to return a not-null value. When we pass a nonexistent key to the function, it throws NoSuchElementException.

Next, let’s create a test to see the usage of this function:

val result = inputMap.getValue(3)
assertTrue { result.startsWith("HP Laptop") }

assertThrows<NoSuchElementException> { inputMap.getValue(42) }

As we can see, this time, we don’t need to append !! after the result variable, as getValue() always returns a not-null value.

What if we’d like to use the get operator to retrieve values? Is it possible to make map[key] return a not-null value also?

The answer is yes. So next, let’s see how to achieve it.

4. Wrapping the Map in an Anonymous Object

We’ve seen earlier that the standard get operator returns a nullable value. So, to make map[key] returns a not-null value, we need to overload the get operator.

Also, Kotlin allows us to define anonymous objects from scratch conveniently. Therefore, we can create an anonymous object wrapping the map object and then overload the get operator. Let’s see how this is done:

val laptops = object {
    val theMap = inputMap
    operator fun get(key: Int): String = theMap[key]!! // throws NPE if the key doesn't exist
}

As we can see, we overload the get operator to return theMap[key]!!. This means we return the value if the key exists in the map. Otherwise, laptops[not-exist-key] throws NullPointerException.

Next, let’s write a test to verify if it works as expected:

val result = laptops[3]
assertTrue { result.startsWith("HP Laptop") }
assertThrows<NullPointerException> { laptops[42] }

As laptops‘s get operator gives a not-null value, we don’t need the !! after the result variable. We’ve made it!

However, here comes a new inconvenience. As we created an anonymous object, the type of the variable laptops is Any instead of Map:

assertIsNot<Map<String, String>>(laptops)

That is to say, we cannot access standard Map‘s properties and functions directly. For example, “laptops.size” doesn’t compile. If we need to access Map’s features, we must navigate to the inner theMap variable, like “laptops.theMap.size“.

Next, let’s see whether we can make further improvements to this approach so that we can utilize all Map features and get a not-null value by map[key].

5. Creating the NotNullMap Class and Delegating to Map

We’ll use the delegation pattern to achieve our goal. The idea is we create a new class, say NotNullMap, and use delegation to make NotNullMap enhance the Map implementation. Of course, we need to overload the get operator to return a not-null value, like what we did in the anonymous object approach:

class NotNullMap<K, V>(private val map: Map<K, V>) : Map<K, V> by map {
    override operator fun get(key: K): V {
        return map[key]!! // throws NPE if the key doesn't exist
    }
}

Next, let’s write a test to see how to use the NotNullMap class:

val laptops = NotNullMap(inputMap)
assertTrue { laptops[3].startsWith("HP Laptop") }
assertThrows<NullPointerException> { laptops[42] }

As we can see, we can simply use the original map object to construct a NotNullMap instance. As we’ve overridden the get operator, laptops[3] returns a not-null value.

Moreover, a NotNullMap instance is still in type Map<K, V>. So we can access all Map features directly:

assertIs<Map<Int, String>>(laptops)
assertEquals(4, laptops.size)

The delegation approach is pretty close to our expectations now. But there is still one inconvenience. We have seen that we can create a NotNullMap object by passing a Map object to the constructor. In other words, when we want to use NotNullMap, we need a Map instance first.

It can be handy if we can directly initialize NotNullMap from data pairs, like creating a Map, such as notNullMapOf( k1 to v1, k2 to v2 ….).

So next, let’s create a factory function to make that happen:

// Provides the same factory function for creating a NotNullMap just like creating Map by mapOf()
fun <K, V> notNullMapOf(vararg pairs: Pair<K, V>) = NotNullMap(mapOf(*pairs))

We’ve used Kotlins vararg parameter and the spread operator*‘ in the notNullMapOf() function.

Finally, let’s initialize a new NotNullMap instance using this function and perform some operations in a test:

val personHobbies = notNullMapOf(
  "Tom" to setOf("Reading", "hiking"),
  "Jackson" to setOf("Reading", "Football"),
  "Anna" to setOf("Singing", "Tennis")
)

assertEquals(2, personHobbies["Tom"].size)
assertThrows<NullPointerException> { personHobbies["Shirley"] }

6. Conclusion

In this article, we had a closer look at Kotlin’s get operator. We’ve also explored different approaches to get a not-null value from a Map.

As usual, the implementation of all these examples is over on GitHub.

Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.