The type of a Map's key is invariant. That means a Map<B, B>
is not a Map<A, B>
or Map<A, A>
because you cannot upcast an invariant type. Theoretically, the implementation of the Map interface being used could crash when passing it the wrong type of key, like if you passed it some subtype of A that is not a B.
When you call toMap
, it creates a new Map, for which it is known to be safe to use the supertype A as a Key, so it can upcast the type safely. Under the hood, it is transferring each entry to a new map, so it's basically up-casting each of the keys to type A
.
Here's an example of what the type safety protects you from:
interface A
class B(val name: String): A
class C: A
class MyMap: HashMap<B, B>() {
override fun get(key: B): B? {
println("I'm returning ${key.name}")
return super.get(key)
}
}
If you now did this and the compiler let you:
val a = Map<A, A>
val b: Map<B, B> = MyMap()
a = b // imagine this is allowed.
val x = a[C()] // Crash. C cannot be cast to B inside the MyMap.get() function
If you use toMap()
, a new Map is being created from scratch and it will not have this problem so it is safe for the compiler to upcast the key type.
Java doesn't have this problem because get
and contains
, etc. do not take argument types of the key type, but accept anything. There are pros and cons to the two approaches. They each protect you from different types of bugs.