I'm new to Kotlin. When I learn Storing Properties in a Map. I try following usage.
class User(val map: MutableMap<String, String>) {
val name: String by map
}
class User(val map: MutableMap<String, in String>) {
val name: String by map
}
class User(val map: MutableMap<String, out String>) {
val name: String by map
}
The first two are both work, the last one failed.
With out
modifier, the bytecode of getName
like this:
public final java.lang.String getName();
0 aload_0 [this]
1 getfield kotl.User.name$delegate : java.util.Map [11]
4 astore_1
5 aload_0 [this]
6 astore_2
7 getstatic kotl.User.$$delegatedProperties : kotlin.reflect.KProperty[] [15]
10 iconst_0
11 aaload
12 astore_3
13 aload_1
14 aload_3
15 invokeinterface kotlin.reflect.KProperty.getName() : java.lang.String [19] [nargs: 1]
20 invokestatic kotlin.collections.MapsKt.getOrImplicitDefaultNullable(java.util.Map, java.lang.Object) : java.lang.Object [25]
23 checkcast java.lang.Object [4]
26 aconst_null
27 athrow
Local variable table:
[pc: 0, pc: 28] local: this index: 0 type: kotl.User
As we can see, it will cause a NullPointerException
.
Why contravariant is not allowed on a map delegate?
And why kotlin doesn't give me a compile error?
Yeah... the compiler is definitely wrong here. (testing with Kotlin version 1.1.2-5)
First of all, in the case of property delegation to a map, you use the name of the property to look up a value for it in the map.
Using a MutableMap<String, in String>
, is equivalent to Java's Map<String, ? super String>
which uses contravariance.
Using a MutableMap<String, out String>
, is equivalent to Java's Map<String, ? extends String>
which uses covariance.
(you mixed the two up)
A covariant type can be used as a producer. A contravariant type can be used as a consumer. (See PECS. sorry, I don't have a Kotlin specific link, but the principle still applies).
Delegation by map uses the second generic type of map as a producer (you get things out of the map), so it should not be possible to use a MutableMap<String, in String>
since it's second parameter is a consumer (to put things into).
For some reason, the compiler generates the code it needs for a MutableMap<String, out String>
in the case of a MutableMap<String, in String>
and this is wrong, as you can see in this example:
class User(val map: MutableMap<String, in String>) {
val name: String by map
}
fun main(args:Array<String>){
val m: MutableMap<String, CharSequence> = mutableMapOf("name" to StringBuilder())
val a = User(m)
val s: String = a.name
}
You will get a class cast exception, because the VM is trying to treat a StringBuilder
as a String
. But you don't use any explicit casts, so it should be safe.
Unfortunately, it generates garbage (throw null
) in the valid use case of out
.
In the case of String
it doesn't really make sense to use covariance (out
), since String
is final, but in the case of a different type hierarchy, the only work around I can think of is to manually patch the bytecode, which is a nightmare.
I don't know if there is an existing bug report. I guess we'll just have to wait until this gets fixed.