Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Kotlin: Reified generics don't seem to work right for hash/equals comparisons

I have a map of KClass to Int. I then have a function which has a reified generic type. I'd then expect the following situation, thus, to give me the Int associated with Boolean::class

val kclassToInt = mapOf(Boolean::class to 1, Byte::class to 1, Short::class to 2)

inline fun <reified T> myExpectations() =
        assertEquals(1, kclassToInt.getRaw(T::class), "Why doesn't it work? :'(")

I'm greeted with Why doesn't it work? :'(. Expected <1>, actual <null>. from a call to it like this myExpectations<Boolean>().

I then tried to use .java off, so I was using Java's Class rather than Kotlin's KClass.

val classToInt = mapOf(Boolean::class.java to 1, Byte::class.java to 1, Short::class.java to 2)

inline fun <reified T : Any> anotherExpectation() =
        assertEquals(1, classToInt.getRaw(T::class.java))

This time I was again greeted by assertion error: java.lang.AssertionError: Expected <1>, actual <null>.

Finally I tried using .javaClass rather than .java:

val javaClassToInt = mapOf(Boolean::class.javaClass to 1, Byte::class.javaClass to 1, Short::class.javaClass to 2)

inline fun <reified T> pleaseWork() =
        assertEquals(1, javaClassToInt.getRaw(T::class.javaClass))

This time it was really strange. I was greeted with this: java.lang.AssertionError: Expected <1>, actual <2>. This seems to be because all .javaClass refer to KClassImpl.

Finally I resorted to what I didn't want to do, use .qualifiedName:

val qnToInt = mapOf(Boolean::class.qualifiedName to 1, Byte::class.qualifiedName to 1, Short::class.qualifiedName to 2)

inline fun <reified T> iKnowItWorks() =
        assertEquals(1, qnToInt.getRaw(T::class.qualifiedName))

Which of course works and is what I use in my actual use case: https://github.com/Jire/kotmem/blob/master/src/main/kotlin/org/jire/kotmem/Process.kt

like image 681
Jire Avatar asked Sep 26 '22 02:09

Jire


2 Answers

Most likely you've written something like println(type.javaClass) which may seem to make sense, but actually doesn't because it always prints class kotlin.reflect.jvm.internal.KClassImpl, since that is the internal implementation class of the KClass interface.

Why does type.javaClass work that way? javaClass is an extension property which gets a runtime Java class of any value passed to it as a receiver. Its signature is:

val <T : Any> T.javaClass: Class<T>

type is a perfectly valid value of type KClass<T>, so type.javaClass's resulting type is Class<KClass<T>>. This is already almost entirely meaningless unless you want to introspect symbols of the KClass implementation class. Since type is a KClassImpl instance at runtime, type.javaClass is effectively a Class instance representing the class named kotlin.reflect.jvm.internal.KClassImpl.

This is a bit confusing and definitely not what you wanted to do. If you want to print a class instance to the screen, you can just call println(type). If you want to obtain a Java Class instance corresponding to the KClass instance you have, you can use the java extension property: type.java. java's signature is:

val <T : Any> KClass<T>.java: Class<T>

So if type is a KClass<T>, then type.java is a Class<T>.

like image 173
Alexander Udalov Avatar answered Sep 29 '22 06:09

Alexander Udalov


I believe that the key types in your Map are KClass instances for the primitive types (Java int rather than Integer). The reified type in the function is the KClass instance for the boxed type (Integer), as seen in Are Kotlin's reified types incorrect for primitives on the JVM?

Whilst these two KClasss print as the same thing, they are not equal, so your lookup fails.

like image 21
Duncan McGregor Avatar answered Sep 29 '22 07:09

Duncan McGregor