kotlin.time.Duration 和 java 反射

use*_*253 5 java reflection kotlin

似乎我在 java 反射 API 的交互中遇到了一个奇怪的问题(特别是java.lang.reflect.Proxykotlin.time.Duration。看起来 Java 反射无法确定 的方法返回类型kotlin.time.Duration。考虑以下示例:

@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()
}
Run Code Online (Sandbox Code Playgroud)

此代码产生以下输出:

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
Run Code Online (Sandbox Code Playgroud)

如您所见,内部sampleProxy返回类型getNullableDuration()getRange()被正确确定(kotlin.time.Duration?预期变为kotlin.time.Duration),而getDuration()突然变为方法返回double并且从 kotlin.time.Duration 转换为从方法返回时加倍失败并出现异常。

问题的原因是什么?我该如何解决?

我的环境:

  • OpenJDK 11.0.6+10
  • 科特林 1.3.71
  • Linux/x86-64

cac*_*co3 4

Duration是一个inlinedouble

这意味着:

  • Kotlin 编译器将double在可能的情况下使用
  • 如果它不是包装类型,则将使用它

表示部分:

在生成的代码中,Kotlin 编译器为每个内联类保留一个包装器。内联类实例可以在运行时表示为包装器或底层类型。

Kotlin 编译器更喜欢使用底层类型而不是包装器来生成性能最佳且优化的代码。然而,有时有必要保留包装纸。根据经验,内联类在用作另一种类型时都会被装箱。

在提供的示例中:

  • getNullableDuration返回,这意味着对值进行装箱(请参阅 Kotlin 文档的表示Duration?部分中的示例)。
  • getDuration返回仅Duration允许编译器使用基础类型,double而不是包装器。
  • 该行"getNullableDuration", "getDuration" -> 1.seconds返回一个包装器而不是基础类型,因为它InvocationHandler#invoke返回一个对象,这不允许编译器使用基础类型。

这就是被ClassCastException抛出的原因。编译后的getDuration方法返回 a double,但代理将始终返回Duration

下面是IC反编译为 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);
   }
}
Run Code Online (Sandbox Code Playgroud)

解决方法

最直接的一种是使用可为 null 的类型

另一种是返回double方法getDuration

when (method.name) {
     "getNullableDuration" -> 1.seconds
     "getDuration" -> 1.0
     "getRange"-> 0..1
      else -> TODO("method ${method.name} isn't handled yet")
}
Run Code Online (Sandbox Code Playgroud)

请注意,时间 api 和inline类都是实验性的