fun test() {
val map = TreeMap<String, String>()
val key: String? = null
val shouldNotCompile = map[key]
}
This code should not compile, because key of type String? cannot be cast to String. Yet, it compiles, and running this code throws a NullPointerException at runtime.
I believe this is awful, because it ruins null safety. Anyone willing to explain why this happens, or how this issue can be pushed?
I do not see any other reason to define such an extension function. Maybe someone can provide better insight, though, because I am only half-convinced by this explanation.
It compiles because the extension is defined as: Map<out K, V>.get(key: K): V?. Notice out here. It says keys in the map could be subtypes of a key passed as an argument. In other words, it works like this:
// upcasting, Map<out String?, String> is a supertype of Map<String, String>
val map2: Map<out String?, String> = map
val shouldNotCompile = map2.get(key)
But I’m not sure why it was made covariant. Even the implementation immediately casts to invariant, which normally shouldn’t be type-safe. I guess it works only because currently supported target platforms don’t care about the type of passed keys (e.g. in Java it is get(Object)).
Thank you for explaining so far. As I understand, the Kotlin getter does not allow for any object, but only objects that extend the key type K.
The thing I don’t understand is, why null is accepted sometimes. Let me show you a different example:
fun testGet() {
val map = TreeMap<Number, String>()
// works (correct)
val intKey: Int = 3
map[intKey]
// works, but should not (incorrect)
val numberKeyNull: Number? = null
map[numberKeyNull]
// Does not work (correct)
val intKeyNull: Int? = null
// Type inference failed. The value of the type parameter K should be mentioned in input types (argument types, receiver type or expected type). Try to specify it explicitly.
map[intKeyNull]
}
Does this show that Number? extends Number, but Int? does not extend Number?
Is there any reason for not defining the getter extension function like this?
inline fun <K, V> Map<K, V>.get2(key: K): V? = this.get(key)
fun testGet2() {
val map = TreeMap<Number, String>()
// works (correct)
val intKey: Int = 3
map.get2(intKey)
// Does not work (correct)
val numberKeyNull: Number? = null
map.get2(numberKeyNull)
// Does not work (correct)
val intKeyNull: Int? = null
map.get2(intKeyNull)
}
The opposite. This out I mentioned allows to access the map using a supertype of the map’s keys (by upcasting the map). Also, because we can always implicitly upcast the parameter, subtypes are acceptable as well. As a result, we can ask the map for a key which is either a supertype or subtype of its keys.
Int is a subtype of Number, so this is fine. Number? is a supertype of Number. Int? is neither supertype nor subtype.
There is one remaining question. As I said, we can upcast both the map’s key and the parameter, so why it doesn’t go to Any? in your last example? This is disallowed by @OnlyInputTypes annotation. If I understand it correctly, it requires that the K is provided by either the map or the parameter, it can’t be just an arbitrary type.
And again: I don’t know what is the meaning of all of this. I can only explain what is going on in the compiler and why some code compiles and another does not.