I just made an odd discovery and was wondering why it works this way. The following code throws a compiler error:
interface A
class B: A
val mapOfA: Map<A,A>
val mapOfB = mapOf<B,B>()
mapOfA = mapOfB
You get
Type mismatch.
Required: Map<A, A>
Found: Map<B, B>
But this code works.
val mapOfA: Map<A,A>
val mapOfB = mapOf<B,B>()
mapOfA = mapOfB.toMap()
The only difference is now I'm calling mapOfB.toMap()
. mapOfB
is already a Map
so why does that change anything? I'm using Kotlin version 1.5.10
. What's going on here?
CodePudding user response:
Consider mapOfB.get
. This accepts a B
and only a B
.
It is quite possible to have an implementation of mapOfB
that cannot support get(A)
, that has no implementation for it. For example, imagine B
is Int
, and A
is Number
. Imagine mapOfB
is actually implemented in terms of an array. mapOfA.get(3.14159)
certainly can't look up the non-Int
key in an array, since arrays are indexed by Int
s.
(Kotlin chose this design in contrast to Java's design, which I'm not convinced was the right move -- but it's what they chose. Java's choice was for get
, containsKey
, and the like to take an Object
argument, which results in questions like this.)
This is specifically specified in the definition of Map<K, out V>
: upcasting V
is allowed, but not K
.
CodePudding user response:
This is related to types variance. Consider this example:
val mapOfA: Map<A,A>
val mapOfB = mapOf<B,B>()
mapOfA = mapOfB // assume this is allowed
val item = mapOfA.get(A())
We did something weird here. Both variables point at the same map, so we just asked mapOfB
for its A
item. But mapOfB
does not really know anything about A
keys. It was supposed to work with B
keys. It requires B
in its get()
, but we provided A
. Therefore, we just broke the type-safety. This is why this is not allowed.
But why toMap()
works properly? Because it creates a copy of the map. Now, asking mapOfA
for A
key asks only this copy, not map of B
's. So this is allowed.
CodePudding user response:
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.