user3159253
user3159253

Reputation: 17455

kotlin.time.Duration and java reflection

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

Answers (1)

Denis Zavedeev
Denis Zavedeev

Reputation: 8297

Duration is an inline class for double.

This means:

  • the Kotlin compiler will use double where it is possible
  • where it is not a wrapper type will be used instead

From 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.
  • the line "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);
   }
}

Workarounds

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

Related Questions