Map.get accepts null keys - issue is stale

more than a year ago, I created this issue:

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

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

    // 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.

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

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

    // Does not work (correct)
    val intKeyNull: Int? = null

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/ 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:

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