Tomas Karlsson
Tomas Karlsson

Reputation: 755

Kotlin Map with non null values

Let say that I have a Map for translating a letter of a playing card to an integer

 val rank = mapOf("J" to 11, "Q" to 12, "K" to 13, "A" to 14)

When working with the map it seems that I always have to make a null safety check even though the Map and Pair are immutable:

val difference = rank["Q"]!! - rank["K"]!!

I guess this comes from that generic types have Any? supertype. Why can't this be resolved at compile time when both Map and Pair are immutable?

Upvotes: 28

Views: 35418

Answers (6)

ABadHaiku
ABadHaiku

Reputation: 109

This is not determined at compile-time with the current compiler, the logic being that any consumers can only see it by the Map label, and therefore could potentially use it in unintended ways.....even though this absolutely could be part of the compiler, considering TypeScript has this functionality. They probably found it best to not give collection types any special treatment, though, given the number of different map and list subtypes you could have.

Anyway, there are other cases where you are absolutely certain that you physically can never receive a null value from a map, such as if a default value is provided (through a way that isn't Map#withDefault) or when you've covered all possible values of an enum as your keys.

In such cases, wrapping it in a value class can allow you to retrieve values without making a null check:

@JvmInline
value class NonNullMap<T : Any, U : Any?>(val original: Map<T, U>): Map<T, U> by original {
    override operator fun get(key: T): U {
        return internal[key]!!
    }
}

Then you use it like this:

val wrappedMap = NonNullMap(mapOf(
  NORTH to a,
  SOUTH to b,
  EAST to c,
  WEST to d
))

Make sure to only use this when there is physically no way to extract a null value from said map, as this essentially just circumvents the nullability checker.

If there is any chance of actually yielding a null value - such as if your target environment is fraught with ASM usage, where people could potentially inject enum values and break your code - it's always best to add a default and use getValue on the off-chance that a null value somehow slips through.

Upvotes: 0

Bryan M.
Bryan M.

Reputation: 1

I've found a decent solution:

val rank = object {
    val rankMap = mapOf("J" to 11, "Q" to 12, "K" to 13, "A" to 14)
    operator fun get(key: String): Int = rankMap[key]!!
}
val difference = rank["Q"] - rank["K"]

Upvotes: 0

Nurlan
Nurlan

Reputation: 803

There is another method for getting not null value from map:

fun <K, V> Map<K, V>.getValue(key: K): V

throws NoSuchElementException - when the map doesn't contain a value for the specified key and no implicit default value was provided for that map.

but operator for get == map[] returns nullable.

operator fun <K, V> Map<out K, V>.get(key: K): V?

Upvotes: 32

ericn
ericn

Reputation: 13103

The short answer is you can't achieve that until Kotlin changes. As others have pointed out, this doesn't have to do with mutability but the fact that Java's Maps accept null as valid values. At the moment, Kotlin's *Map classes has the exact implementation as Java's *Map classes.

If you still want to achieve non-null-value only map, you'll need to implement your own e.g. extending Map or wrap around it

More specifically, behind the scene, mapOf gives us a Kotlin's LinkedHashMap which is not a different class but a just a typealias of Java's LinkedHashMap

Maps.kt

public fun <K, V> mapOf(vararg pairs: Pair<K, V>): Map<K, V> =
    if (pairs.size > 0) pairs.toMap(LinkedHashMap(mapCapacity(pairs.size))) else emptyMap()

TypeAliases.kt

@SinceKotlin("1.1") public actual typealias LinkedHashMap<K, V> = java.util.LinkedHashMap<K, V>

You can try map.getValue(key) instead of map.get(key) but I personally think that's unclean and confusing.

Perhaps some others from Dan Lew here would be useful for you?

My Kotlin version is 1.3.72-release-IJ2020.1-3

Upvotes: 0

mbStavola
mbStavola

Reputation: 368

mapOf() is providing a Map with no guarantees for the presence of a key-- something which is kind of expected especially considering the Java implementation of Map.

While I might personally prefer sticking with null-safe calls and elvis operators, it sounds like you'd prefer cleaner code at the call site (especially considering you know these keys exist and have associated non-null values). Consider this:

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

By delegating to an implementation of map, but overriding the get method, we can guarantee that return values are non-null. This means you no longer have to worry about !!, ?., or ?: for your usecase.

Some simple test code shows this to be true:

fun main(args: Array<String>) { 
    val rank = nonNullMapOf("J" to 11, "Q" to 12, "K" to 13, "A" to 14)
    val jackValue: Int = rank["J"] // Works as expected
    println(jackValue)
    val paladinValue: Int = rank["P"] // Throws an NPE if it's not found, but chained calls are considered "safe"
    println(jackValue)
}

// Provides the same interface for creating a NonNullMap as mapOf() does for Map
fun <K, V> nonNullMapOf(vararg pairs: Pair<K, V>) = NonNullMap(mapOf<K, V>(*pairs))

Upvotes: 12

Rafal G.
Rafal G.

Reputation: 4432

It is not about the implementation of Map (being it Kotlin or Java based). You are using a Map and a map may not have a key hence [] operator returns nullable type.

Upvotes: 18

Related Questions