Map.get accepts null keys - issue is stale

Hey,
more than a year ago, I created this issue: https://youtrack.jetbrains.com/issue/KT-52231/Map.get-accepts-null-keys-although-forbidden-by-type

In short:

fun test() {
    val map = TreeMap<String, String>()
    val key: String? = null
    val shouldNotCompile = map[key]
}

This code should not compile, because key of type String? cannot be cast to String. Yet, it compiles, and running this code throws a NullPointerException at runtime.

I believe this is awful, because it ruins null safety. Anyone willing to explain why this happens, or how this issue can be pushed?

I am not sure, but I think this is a problem caused by Java interop.

If I understand well, Kotlin defines a get extension that accept any type of key to match java.util.Map#get(Object).

I do not see any other reason to define such an extension function. Maybe someone can provide better insight, though, because I am only half-convinced by this explanation.

It compiles because the extension is defined as: Map<out K, V>.get(key: K): V?. Notice out here. It says keys in the map could be subtypes of a key passed as an argument. In other words, it works like this:

// upcasting, Map<out String?, String> is a supertype of Map<String, String>
val map2: Map<out String?, String> = map
val shouldNotCompile = map2.get(key)

But I’m not sure why it was made covariant. Even the implementation immediately casts to invariant, which normally shouldn’t be type-safe. I guess it works only because currently supported target platforms don’t care about the type of passed keys (e.g. in Java it is get(Object)).

Thank you for explaining so far. As I understand, the Kotlin getter does not allow for any object, but only objects that extend the key type K.
The thing I don’t understand is, why null is accepted sometimes. Let me show you a different example:

fun testGet() {
    val map = TreeMap<Number, String>()

    // works (correct)
    val intKey: Int = 3
    map[intKey]

    // works, but should not (incorrect)
    val numberKeyNull: Number? = null
    map[numberKeyNull]

    // Does not work (correct)
    val intKeyNull: Int? = null
    // Type inference failed. The value of the type parameter K should be mentioned in input types (argument types, receiver type or expected type). Try to specify it explicitly.
    map[intKeyNull]
}

Does this show that Number? extends Number, but Int? does not extend Number?

Is there any reason for not defining the getter extension function like this?

inline fun <K, V> Map<K, V>.get2(key: K): V? = this.get(key)
fun testGet2() {
    val map = TreeMap<Number, String>()

    // works (correct)
    val intKey: Int = 3
    map.get2(intKey)

    // Does not work (correct)
    val numberKeyNull: Number? = null
    map.get2(numberKeyNull)

    // Does not work (correct)
    val intKeyNull: Int? = null
    map.get2(intKeyNull)
}

This seems to work exactly as I would expect.

The opposite. This out I mentioned allows to access the map using a supertype of the map’s keys (by upcasting the map). Also, because we can always implicitly upcast the parameter, subtypes are acceptable as well. As a result, we can ask the map for a key which is either a supertype or subtype of its keys.

Int is a subtype of Number, so this is fine. Number? is a supertype of Number. Int? is neither supertype nor subtype.

There is one remaining question. As I said, we can upcast both the map’s key and the parameter, so why it doesn’t go to Any? in your last example? This is disallowed by @OnlyInputTypes annotation. If I understand it correctly, it requires that the K is provided by either the map or the parameter, it can’t be just an arbitrary type.

And again: I don’t know what is the meaning of all of this. I can only explain what is going on in the compiler and why some code compiles and another does not.

1 Like

Thank you for explaining. Then I do not understand, why the getter allows supertypes. I believe it should either be Map<K, V> or Map<Any, V>.

What makes this even more confusing is the fact that there are three different declarations of get.

  1. java/util/Map.java declares it as V get(Object key);
  2. kotlin/Collections.kt narrows it down to fun get(key: K): V?
  3. kotlin/collections/Maps.kt supports an extension function Map<out K, V>.get(key: K): V? that opens it up again partially.

Both kotlin declarations are more narrow than the Java one, and for me, the kotlin extension function is pointless / wrong. Any thoughts?

Or, one suggestion to “fix” the extension function by forcing K to extend Any, excluding Null:

@kotlin.internal.InlineOnly
public inline operator fun <@kotlin.internal.OnlyInputTypes K : Any, V> Map<out K, V>.get(key: K): V? =
    @Suppress("UNCHECKED_CAST") (this as Map<K, V>).get(key)

Still not sure if there is a valid use case for this function at all.

1 Like