Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does 'get' method of class 'Map' enable sending a not relevant key without a compilation error?

I just had a bug in my program, but i don't understand how the program was compiled at all!

I have the following variable:

gamesPerCountriesMap: MutableMap<Long, MutableMap<Long, MutableList<AllScoresGameObj>>>?

and i had the following line of code:

var gamesList = gamesPerCountriesMap?.get(countryItem.id)?.get(competitionItem)

the correct line should be:

var gamesList = gamesPerCountriesMap?.get(countryItem.id)?.get(competitionItem.id)

i have looked at the prototype of the Map class and the method is declared as following:

public inline operator fun <@kotlin.internal.OnlyInputTypes K, V> Map<out K, V>.get(key: K): V?

As we can see it can get K and it's subtype, but competitionItem which is an instacne of class CompetitionObj isn't inherit the Long class. So why the compiler didn't prevent this error? I solved the issue but i am very couries of what is didn't prevent the code from being compiled?

like image 723
Eitanos30 Avatar asked Nov 20 '20 20:11

Eitanos30


1 Answers

There are two get methods for Map interface.

One is defined directly in the body of the interface:

Map<K, V> { fun get(key: K): V? }

Another (which you cite in your question) - as an extention function:

fun <K, V> Map<out K, V>.get(key: K): V?

The overload resolution on call depends on whether or not you explicitly specify generic parameters, and on relationship between type of map K generic parameter & type of passed key argument:

  1. If you specify explicit generic parameter, the second version will be called (and it wouldn't have compiled in your case, if you've wrote .get()<Long, MutableList<AllScoresGameObj>.(competitionItem), although .get()<CompetitionObj, MutableList<AllScoresGameObj>.(competitionItem)) would've worked, cause there is unsafe cast inside this get overload).
  2. If you omit explicit generic parameter, passing as a key:
    1. an instance of K type (or its subtype) - the first overload will be called
    2. anything else - the second overload (cause the first overload will not compile). In this case Kotlin will try to infer omitted generic parameters, so that original map could be represented as a Map<out inferredK, V> and inferredK type parameter was a supertype of passed key argument. Eventually, it will come up with inferredK = Any. Indeed Any is a supertype of everything, and it's perfectly legal to do val x: Map<out Any, V> = mapOf<K, V>() for any K. Actually compiler realizes that this is a trivial solution and issues a compilation warning 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.(I believe in your case this warning should've been too). Why this still works in runtime? Because of type erasure.

So, why this overloaded version was added to stdlib? Don't know for sure, maybe for some legal cases like:

val k : Base = Derived()
val v = mapOf<Derived, String>().get(k) // will call overloaded variant; will infer K as Base

without this overload you'd have to manually cast back:

val v = mapOf<Derived, String>().get(k as Derived) 
like image 132
Михаил Нафталь Avatar answered Nov 08 '22 22:11

Михаил Нафталь