In the following code, where MyMap
trivially implements Map
by
delegation to impl
:
foo@host:/tmp$ cat Foo.kt
class MyMap <K, V> (val impl : Map <K, V>) : Map<K, V> by impl {
fun myGetValue (k: K) = impl.getValue(k)
}
fun main() {
val my_map = MyMap(mapOf('a' to 1, 'b' to 2).withDefault { 42 })
println(my_map.myGetValue('c')) // OK
println(my_map.getValue('c')) // ERROR
}
Why do I get the following error on the second println?
foo@host:/tmp$ /path/to/kotlinc Foo.kt
foo@host:/tmp$ /path/to/kotlin FooKt
42
Exception in thread "main" java.util.NoSuchElementException: Key c is missing in the map.
at kotlin.collections.MapsKt__MapWithDefaultKt.getOrImplicitDefaultNullable(MapWithDefault.kt:24)
at kotlin.collections.MapsKt__MapsKt.getValue(Maps.kt:344)
at FooKt.main(Foo.kt:8)
at FooKt.main(Foo.kt)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at org.jetbrains.kotlin.runner.AbstractRunner.run(runners.kt:64)
at org.jetbrains.kotlin.runner.Main.run(Main.kt:176)
at org.jetbrains.kotlin.runner.Main.main(Main.kt:186)
foo@bigdev:/tmp$
Update: The compiler and runtime version outputs are:
foo@host:/tmp$ kotlinc -version
info: kotlinc-jvm 1.6.10 (JRE 17.0.1+12-LTS)
foo@host:/tmp$ kotlin -version
Kotlin version 1.6.10-release-923 (JRE 17.0.1+12-LTS)
foo@host:/tmp$ javac -version
javac 17.0.1
foo@host:/tmp$ java -version
openjdk version "17.0.1" 2021-10-19 LTS
OpenJDK Runtime Environment Corretto-17.0.1.12.1 (build 17.0.1+12-LTS)
OpenJDK 64-Bit Server VM Corretto-17.0.1.12.1 (build 17.0.1+12-LTS, mixed mode, sharing)
This is occurring because of the slightly unexpected way in which withDefault
is implemented. The wrapper that withDefault
produces doesn't override getValue()
as this is impossible because getValue()
is an extension function. So unfortunately, what we have instead is a classic OOP anti-pattern: getValue()
does an is
check to see if it's being called on the internal MapWithDefault
interface, and only uses the default value if that is the case. I don't see any way they could have avoided this situation without breaking the Map contract.
myGetValue
calls getValue
on the underlying delegate, which is a MapWithDefault
, so it works fine.
getValue
called on your MyMap
instance will fail the internal is MapWithDefault
check because MyMap
is not a MapWithDefault
, even though its delegate is. The delegates other types are not propagated up to the class that delegates to it, which makes sense. Like if we delegated to a MutableMap, we might want the class to be considered only a read-only Map.