Map<*, *>.get not calling get method on concrete Map class

Map.get doesn’t behave exactly as specified. Here is an example:

class TrickyMap : Map<Int, Int> {

    override fun get(key: Int): Int? {
        val value = key * 10
        return if (key in setOf(1, 2)) value
        else null
    }

    override val entries: Set<Map.Entry<Int, Int>>
        get() = TODO("Not yet implemented")
    override val keys: Set<Int>
        get() = TODO("Not yet implemented")
    override val size: Int
        get() = TODO("Not yet implemented")
    override val values: Collection<Int>
        get() = TODO("Not yet implemented")
    override fun containsKey(key: Int): Boolean =
        TODO("Not yet implemented")
    override fun containsValue(value: Int): Boolean =
        TODO("Not yet implemented")
    override fun isEmpty(): Boolean =
        TODO("Not yet implemented")
}

fun main() {
    val map: Map<*, *> = TrickyMap()
    println(map[1])  // prints 10
    println(map[3])  // prints null
    println(map["a"])  // should throw a ClassCastException but prints null
}

By tracing the code, it can seen that map[1], map[3], and map[“a”] call the extension function

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

in the file Maps.kt. This function then delegates to the get method on the map object.

For map[1] and map[3], the call ends up handled by TrickyMap.get. For map[“a”], however, the call is not delegated to TrickyMap.get but it mysteriously returns null.

Question: What is the explanation for this behaviour? What function is handling the call map[“a”] after delegation by the above extension function?

In Kotlin JVM, the interface kotlin.collections.Map does not actually exist in byte code. Instead, both Map and MutableMap are compiled to java.util.Map. Unlike in the Kotlin interfaces, java.util.Map#get has Object as parameter type rather than K. Since the Kotlin interface does not require this method to be defined by the implementor, the compiler implements it under the hood. If you decompile the generated class file to Java, you will see that it contains the following method:

// $FF: bridge method
public final Object get(Object var1) {
   return var1 instanceof Integer ? this.get(((Number)var1).intValue()) : null;
}

3 Likes

Thank you, Varia. This explains the “mysterious” behaviour for map[“a”] in my example.