Reputation: 3356
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)
Upvotes: 3
Views: 911
Reputation: 93902
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.
Upvotes: 2
Reputation: 27246
Though I would have expected your code to work to be honest, this could be bug but we'd have to look at the produced bytecode.
In the documentation it says (emphasis mine):
This implicit default value is used when the original map doesn't contain a value for the key specified and a value is obtained with Map.getValue function, for example when properties are delegated to the map.
The conflict of "contracts" comes from the actual Map
interface, which says:
Returns the value corresponding to the given [key], or null if such a key is not present in the map.
The maps default contract must fulfill this, so it can "only" return null when a key is non-existent.
I have found one discussion about this in the Kotlin forums.
Upvotes: 2