Reputation: 17455
It seems I've encountered a weird problem with interaction of java reflection API (particularly, java.lang.reflect.Proxy and kotlin.time.Duration. It looks like Java Reflection fails to determine method return type for kotlin.time.Duration
. Consider the following example:
@file:JvmName("Main")
package org.test.kotlin.time.duration
import java.lang.reflect.Proxy
import kotlin.time.Duration
import kotlin.time.ExperimentalTime
import kotlin.time.seconds
@ExperimentalTime
interface I {
fun getNullableDuration(): Duration? = 1.seconds
fun getRange(): IntRange = 1..3
fun getDuration(): Duration = 1.seconds
}
class IC: I
inline fun <reified T> T.sampleProxy(): T = sampleProxy(T::class.java)
fun <T> sampleProxy(c: Class<T>): T {
return c.cast(Proxy.newProxyInstance(c.classLoader, arrayOf(c)) { _, method, _ ->
println("[proxy] ${method.declaringClass.name}::${method.name} return type is ${method.returnType}")
when (method.name) {
"getNullableDuration", "getDuration" -> 1.seconds
"getRange" -> 0..1
else -> TODO("method ${method.name} isn't handled yet")
}
})
}
fun main() {
val v: I = IC()
println("I::getNullableDuration() return type is ${v::getNullableDuration.returnType}")
v.sampleProxy().getNullableDuration()
println("I::getRange() return type is ${v::getRange.returnType}")
v.sampleProxy().getRange()
println("I::getDuration() return type is ${v::getDuration.returnType}")
v.sampleProxy().getDuration()
}
This code produces the following output:
I::getNullableDuration() return type is kotlin.time.Duration?
[proxy] org.test.kotlin.time.duration.I::getNullableDuration return type is class kotlin.time.Duration
I::getRange() return type is kotlin.ranges.IntRange
[proxy] org.test.kotlin.time.duration.I::getRange return type is class kotlin.ranges.IntRange
I::getDuration() return type is kotlin.time.Duration
[proxy] org.test.kotlin.time.duration.I::getDuration return type is double
Exception in thread "main" java.lang.ClassCastException: class kotlin.time.Duration cannot be cast to class java.lang.Double (kotlin.time.Duration is in unnamed module of loader 'app'; java.lang.Double is in module java.base of loader 'bootstrap')
at com.sun.proxy.$Proxy2.getDuration(Unknown Source)
at org.test.kotlin.time.duration.Main.main(main.kt:38)
at org.test.kotlin.time.duration.Main.main(main.kt)
Process finished with exit code 1
As one can see, inside sampleProxy
return types for getNullableDuration()
and getRange()
are determined correctly (kotlin.time.Duration?
expectedly becomes kotlin.time.Duration
), while getDuration()
suddenly becomes a method returning double
and the cast from kotlin.time.Duration to double on return from the method fails with the exception.
What could be a reason for the problem? How can I workaround it?
My environment:
Upvotes: 6
Views: 1851
Reputation: 8297
Duration
is an inline
class for double
.
This means:
double
where it is possibleFrom the representation section:
In generated code, the Kotlin compiler keeps a wrapper for each inline class. Inline class instances can be represented at runtime either as wrappers or as the underlying type.
The Kotlin compiler will prefer using underlying types instead of wrappers to produce the most performant and optimized code. However, sometimes it is necessary to keep wrappers around. As a rule of thumb, inline classes are boxed whenever they are used as another type.
In the provided example:
getNullableDuration
returns Duration?
which means boxing the value (see the example in the representation section of Kotlin documentation.getDuration
returns just Duration
which allows compiler to use the underlying type, double
instead of the wrapper."getNullableDuration", "getDuration" -> 1.seconds
returns a wrapper instead of underlying type because the InvocationHandler#invoke
returns an Object, which does not allow the compiler to use the underlying type.That's why the ClassCastException
thrown. The compiled getDuration
method returns a double
, but the proxy will always return Duration
.
Here is how the IC
looks like when decompiled to Java:
public final class IC implements I {
@Nullable
public Duration getNullableDuration() {
return I.DefaultImpls.getNullableDuration(this);
}
@NotNull
public IntRange getRange() {
return I.DefaultImpls.getRange(this);
}
// vvvvvvvvvv here is the double
public double getDuration() {
return I.DefaultImpls.getDuration(this);
}
}
The most straightforward one is to use a nullable type
Another one is to return double
for the getDuration
method:
when (method.name) {
"getNullableDuration" -> 1.seconds
"getDuration" -> 1.0
"getRange"-> 0..1
else -> TODO("method ${method.name} isn't handled yet")
}
Please note that both the time api and inline
classes are experimential
Upvotes: 4