Have a method that returns List of String like below:
class SampleClass {
fun sampleMethod(): List<String> {
return listOf()
}
}
When I use Kotlin reflection library to get the method signature, I’d expect to see it including return type “java.util.List<java.lang.String>”, but it only has “java.util.List”.
I wrote a test:
@Test
fun `javaMethod() return type`() {
assertEquals(
"java.util.List<java.lang.String>",
SampleClass::sampleMethod.returnType.javaType.toString()
) // this is passing, returnType.javaType gets me what I need
assertTrue(
SampleClass::sampleMethod.javaMethod.toString()
.contains(SampleClass::sampleMethod.returnType.javaType.toString())
) // this is failing, javaMethod does not include the type of return list
}
I think the only problem here is your expectation that toString will somehow print exactly what you need. Method is correctly typed, this is just how it prints itself.
I’m actually surprised the Kotlin reflection works. I thought generics were erased at runtime, so I didn’t think you could get the type parameter of the List at runtime, even using reflection.
Both Kotlin and Java reflection work in this case. Types are erased for local variables, we don’t keep a full type info for them. Also, if a code is generic, that means the type information is not fully known at the compile time, so this is also missing. But if our class, method or field uses generics with specific parameter types, they are retained in the code. They have to be, otherwise we wouldn’t know how to call this specific class or method. So yes, we have a full type information for a method returning List<String>.
BTW, this is exactly how the good old trick worked in Java for easily acquiring a complicated type token, for example: new TypeToken<List<String>>() {}. This code not only instantiates an object, it actually creates an anonymous class, so List<String> is retained in the code.
Interesting… so if I’m understanding you correctly:
fun <T> genericCollection(param: List<T>) {
// don't know what T is here; could be anything. Type is erased at runtime
}
fun stringCollection(param: List<String>) {
// in here, we know that param is List<String>, and if we used reflection to inspect the function parameters, we could see it is List<String>
genericCollection(param)
}
So I guess types aren’t retained when passing by reference, but when they’re part of the signature of something, they are retained.
Yes, this is correct. Actually, these cases are really simple. Whatever we see in the source code, we will see in the bytecode / when using reflection. If we have param: List<String>, reflection will give us List<String>. If we have param: List<T>, we will get List<T>, so we don’t know what it is exactly.
I think the more confusing case for people is if we have a generic class MyClass<T>, then somewhere in the code we create it as: val obj = MyClass<String>(). In this case it feels like this String should be known. But the problem is that def-site and use-site are entirely separate in the code. MyClass internally still “sees” just T, String was never passed to it in any way. And local variables aren’t part of the reflection API, so we can’t ask for their type. Similarly, if we then have another class with a field MyClass<String>, then we can ask this class for the type of its fields and we will know the String. But the MyClass internally still can’t acquire it.
This is very interesting. So basically, the thing that declares a type parameter doesn’t know what that type parameter is at runtime. But a variable declaration of a thing that has a type parameter will retain that type parameter at runtime, which I guess makes sense because it’s part of the variable’s type. val myList: List<String> can’t be assigned a List<Int>, after all.