Map get with null key - kotlin

I'm confused by Kotlin's null safety features when it comes to maps. I have a Map<String, String>. Yet I can call map.get(null) and it returns null to indicate that the key is not present in the map. I expected a compiler error, because map is a Map<String, String> and not a Map<String?, String>. How come I can pass null for a String argument?
And a related question: is there any type of Map, be it a stdlib one or a third-party implementation, that may throw NullPointerException if I call get(null)? I'm wondering if it is safe to call map.get(s) instead of s?.let { map.get(it) }, for any valid implementation of Map.
Update
The compiler does indeed return an error with map.get(null). But that is not because of null safety, but because the literal null doesn't give the compiler an indication of the type of the parameter being passed. My actual code is more like this:
val map: Map<String, String> = ...
val s: String? = null
val t = map.get(s)
The above compiles fine, and returns null. How come, when the key is supposed to be a String which is non-nullable?

The get method in Map is declared like this:
abstract operator fun get(key: K): V?
so for a Map<String, String>, its get method should only take Strings.
However, there is another get extension function, with the receiver type of Map<out K, V>:
operator fun <K, V> Map<out K, V>.get(key: K): V?
The covariant out K is what makes all the difference here. Map<String, String> is a kind of Map<out String?, String>, because String is a subtype of String?. As far as this get is concerned, a map with dogs as its keys "is a" map with animals as its keys.
val notNullableMap = mapOf("1" to "2")
// this compiles, showing that Map<String, String> is a kind of Map<out String?, String>
val nullableMap: Map<out String?, String> = notNullableMap
And this is why you can pass in a String? into map.get, where map is a Map<String, String>. The map gets treated as "a kind of" Map<String?, String> because of the covariant out K.
And a related question: is there any type of Map, be it a stdlib one or a third-party implementation, that may throw NullPointerException if I call get(null)?
Yes, on the JVM, TreeMap (with a comparator that doesn't handle nulls) doesn't support null keys. Compare:
val map = TreeMap<Int, Int>()
println(map[null as Int?]) // exception!
and:
val map = TreeMap<Int, Int>(Comparator.nullsLast(Comparator.naturalOrder()))
println(map[null as Int?]) // null
However, note that since the problematic get is an extension function that is available on every Map, you cannot prevent someone from passing in a nullable thing to your map at compile time, as long as your map implements Map.

Related

Kotlin: universal data type in HashMap?

I have some Hash Maps of some objects <String, Object>, some goes like this:
val mapA = HashMap<String, A>
val mapB = HashMap<String, B>
val mapC = HashMap<String, C>
I want to create a function that takes any Hash Map to print it out. This is what I mean:
fun printHashMap(hashMap: HashMap<String, (any type here, for example A, B or C)>){
// print each element of hashmap
}
What I've tried so far:
fun printHashMap(hashMap: HashMap<Any, Any>){
// print HashMap here
}
But it throws me Type mismatch, <Any, Any> vs <String, A> for example.
Is this possible in Kotlin?
If you don't need specific print format, you can print hashMap without your own function:
val testMap = HashMap<String, Any>()
testMap["1"] = 1
testMap["2"] = "2"
print(testMap)
Otherwise, you should change your signature of method to
fun printHashMap(map: Map<String, *>)
In functions better to use Interface instead of concrete class. And instead of Any you need to use star-projections. It is the safe way here is to define such a projection of the generic type, that every concrete instantiation of that generic type would be a subtype of that projection.
You can read more about it here and here

Type- or Class-keyed map

In Kotlin I can use filterIsInstance to obtain a type-specific (and type-safe) sub-collection:
val misc: List<Any> = listOf(42, 3.14, true, "foo", "bar")
val strings: List<String> = misc.filterIsInstance<String>()
println(strings) // => [foo, bar]
But I have a large collection of objects and I would like to pre-sort it into a Map, by concrete type. Is it even possible to define such a map in Kotlin's type system?
val miscByType: Map<KType, Collection<???>>
or:
val miscByClass: Map<KClass, Collection<???>>
Should I use a custom implementation with unsafe (but logically sound) casts?
The following is such an implementation. It works, but I'm wondering if there is a less hacky way of doing it:
import kotlin.reflect.KClass
class InstanceMap {
// INVARIANT: map from KClass to a set of objects of *that concrete class*
private val map: MutableMap<KClass<*>, MutableSet<Any>> = mutableMapOf()
// this is the only public mutator, it guarantees the invariant
fun add(item: Any): Boolean =
map.getOrPut(item::class) { mutableSetOf() }.add(item)
// public non-inline accessor, only needed by the inline accessor
fun get(cls: KClass<*>): Set<*>? = map[cls]
// inline accessor that performs an unsafe, but sound, cast
#Suppress("UNCHECKED_CAST")
inline fun <reified T> get(): Set<T> = get(T::class) as Set<T>? ?: setOf()
}
fun instanceMapOf(vararg items: Any): InstanceMap = InstanceMap().apply {
items.forEach { add(it) }
}
val misc = instanceMapOf(42, 3.14, true, "foo", "bar")
val strings = misc.get<String>()
println(strings) // => [foo, bar]
Your code looks OK. The only problem is with the unchecked cast warning. At the JVM bytecode level, the cast does nothing, because of the way, how generics are implemented in Java and Kotlin. It is also known as type erasure.
https://en.wikipedia.org/wiki/Type_erasure
Type erasure adds yet another problem to your code - it does not tell generic type arguments. So that, for example, List<Int> has the same class as List<String> or List<Map<String, Object>>
Do you expect your code to find superclasses or interfaces in the map? E.g. if I have
interface A
interface B
class C : A, B
val m = InstanceMap()
m.add(C())
m.get(C::class)
m.get(A::class)
m.get(B::class)
Do you expect these 3 calls to return the same value?
The JVM standard workaround for it is to explicitly pass Class<T> parameter and use the Class#cast method to cast instead. That change will make the code safer.
There is a remedy to type erasure in Kotlin. You may add a reified inline function so that Kotlin compiler will use the exact type in the inlined generic function body
https://kotlinlang.org/docs/reference/inline-functions.html#reified-type-parameters
inline fun <reified T> InstanceMap.get() = get(T::class)

obtain class from nested type parameters in kotlin

I have a val built like this
val qs = hashMapOf<KProperty1<ProfileModel.PersonalInfo, *> ,Question>()
How can I obtain the class of ProfileModel.PersonalInfo from this variable?
In other words what expression(involving qs of course) should replace Any so that this test passes.
#Test
fun obtaionResultTypeFromQuestionList(){
val resultType = Any()
assertEquals(ProfileModel.PersonalInfo::class, resultType)
}
Thank you for your attention
There is no straight way to get such information due to Java type erasure.
To be short - all information about generics (in your case) is unavailable at runtime and HashMap<String, String> becomes HashMap.
But if you do some changes on JVM-level, like defining new class, information about actual type parameters is kept. It gives you ability to do some hacks like this:
val toResolve = object : HashMap<KProperty1<ProfileModel.PersonalInfo, *> ,Question>() {
init {
//fill your data here
}
}
val parameterized = toResolve::class.java.genericSuperclass as ParameterizedType
val property = parameterized.actualTypeArguments[0] as ParameterizedType
print(property.actualTypeArguments[0])
prints ProfileModel.PersonalInfo.
Explanation:
We define new anonymous class which impacts JVM-level, not only runtime, so info about generic is left
We get generic supperclass of our new anonymous class instance what results in HashMap< ... , ... >
We get first type which is passed to HashMap generic brackets. It gives us KProperty1< ... , ... >
Do previous step with KProperty1
Kotlin is tied to the JVM type erasure as well as Java does. You can do a code a bit nice by moving creation of hash map to separate function:
inline fun <reified K, reified V> genericHashMapOf(
vararg pairs: Pair<K, V>
): HashMap<K, V> = object : HashMap<K, V>() {
init {
putAll(pairs)
}
}
...
val hashMap = genericHashMapOf(something to something)

Inferring a generic type of Map in Kotlin

Consider a Java method which infers its type by Java class as follows:
public <T> T readJson(Class<T> c) throws IOException {
This allows doing something like this:
Map<String, String> map = foo.readJson(Map.class);
In java it will warn about unchecked cast, but it will work correctly. However in Kotlin, this will not be so easy, one could try using:
foo.readJson(Map::class.java)
However if Map<String, String> will be required, it will not work:
Type inference failed. Expected type mismatch.
required Map<String, String>
found Map<*, *>!
I also tried defining an interface StringMap:
interface StringMap : Map<String, String>
However that does not work either, it will lead to exceptions like this:
Cannot cast ...LinkedTreeMap to ...StringMap
What would be a correct way of doing this?
Kotlin does not have anything like Java raw types (which were left in Java for backward compatibility), and the type system therefore does not allow this kind of unchecked assignment to be made implicitly (star projections, the closest concept to raw types in Kotlin, retain type safety).
You can make an unchecked cast to Map<String, String>, thus expressing that you are aware of a possible type mismatch at runtime:
#Suppress("UNCHECKED_CAST")
val result = foo.readJson(Map::class.java) as Map<String, String>
You can suppress the unchecked cast warning for a broader scope than just one statement.
A natural improvement of this solution is writing a util function to hide the unchecked cast in it:
#Suppress("UNCHECKED_CAST")
inline fun <reified T: Any> JsonReader.readJson(): T {
val result = readJson(T::class.java)
return result as T
}
This solution uses an inline function with a reified type parameter: the function is transformed and substituted at each of its call sites, with T replaced by the specified (or inferred) type at compile time .
Usage examples:
val map = jsonReader.readJson<Map<String, String>>()
fun processMap(map: Map<String, String) { /* ... */ }
processMap(jsonReader.readJson()) // Map<String, String> is inferred for this call

In Kotlin, how do I idiomatically access nullable nested map values, or return a default?

Quick Kotlin best practices question, as I couldn't really work out the best way to do this from the documentation.
Assume I have the following nested map (typing specified explicitly for the purpose of this question):
val userWidgetCount: Map<String, Map<String, Int>> = mapOf(
"rikbrown" to mapOf(
"widgetTypeA" to 1,
"widgetTypeB" to 2))
Can the following mode be any more succinct?
fun getUserWidgetCount(username: String, widgetType: String): Int {
return userWidgetCount[username]?.get(widgetType)?:0
}
In other words, I want to return the user widget count iff the user is known and they have an entry for that widget type, otherwise zero. In particular I saw I can use [] syntax to access the map initially, but I couldn't see a way to do this at the second level after using ?..
I would use an extension operator method for that.
// Option 1
operator fun <K, V> Map<K, V>?.get(key: K) = this?.get(key)
// Option 2
operator fun <K, K2, V> Map<K, Map<K2, V>>.get(key1: K, key2: K2): V? = get(key1)?.get(key2)
Option 1:
Define an extension that provides get operator for nullable map. In Kotlin's stdlib such approach appears with Any?.toString() extension method.
fun getUserWidgetCount(username: String, widgetType: String): Int {
return userWidgetCount[username][widgetType] ?: 0
}
Option 2:
Create a special extension for map of maps. In my opinion, it is better because it shows the contract of the map of maps better than two gets in a row.
fun getUserWidgetCount(username: String, widgetType: String): Int {
return userWidgetCount[username, widgetType] ?: 0
}