Reputation: 755
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
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
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
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
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
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
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