Is there a way to get a Kotlin KType
from a java.lang.reflect.Type
?
Background: I'm working on some code that gets properties/fields from a class and passes them to methods (think JUnit 4 Theories for anyone familiar with those). Because I need Java compatibility, I can't always use pure Kotlin reflection because calls like MyClass::class.memberProperties
don't work with raw Java fields.
Another option would be to cast my KType
s to java.lang.reflect.Type
s (via .javaType
), butKType
provides a convenient isSupertypeOf(...)
function that allows comparison between two KTypes
and I haven't found similar functionality for Java's Type
.
Very Rough Example:
Kotlin Code:
class KotlinTestClass {
fun test(someParam: List<String>) {
someParam.forEach { println(it) }
}
class CompatibleTypes {
val exact: List<String> = listOf("foo")
val mutable: MutableList<String> = mutableListOf("bar")
val implementation: ImmutableList<String> = immutableListOf("baz")
}
class NonCompatibleTypes {
val wrongParam: List<Int> = listOf(42)
val wrongType: Map<String, String> = mapOf("hello" to "world")
}
}
Java code:
public class JavaTestClass {
public static class CompatibleTypes {
public static final List<String> exact = List.of("foo");
public static final ImmutableList<String> implementation = ImmutableList.of("baz");
}
public static class NonCompatibleTypes {
public static final List<Integer> wrongParam = List.of(42);
public static final Map<String,String> wrongType = Map.of("hello", "world");
}
}
Kotlin Test Code:
fun main(args: Array<String>) {
val parameterType = KotlinTestClass::class.functions.first { it.name == "test" }.parameters[1].type
println("Target: $parameterType")
println("--------------------------------------")
//These should all print "true"
println("Compatible Kotlin types:")
KotlinTestClass.CompatibleTypes::class.memberProperties.asSequence()
.map { it.returnType }
.forEach { println(" - ${parameterType.isSupertypeOf(it)} ($it)") }
println("--------------------------------------")
println("Compatible Java types:")
//These should all print "true"
//This code doesn't work because the fields aren't valid Kotlin properties
JavaTestClass.CompatibleTypes::class.memberProperties.forEach { println(it) }
JavaTestClass.CompatibleTypes::class.java.declaredFields.asSequence()
.map { it.kotlinProperty?.returnType }
.filterNotNull()
.forEach { println(" - ${parameterType.isSupertypeOf(it)} ($it)") }
println("--------------------------------------")
println("Non-Compatible Kotlin types:")
//These should all print "false"
KotlinTestClass.NonCompatibleTypes::class.memberProperties.asSequence()
.map { it.returnType }
.forEach { println(" - ${parameterType.isSupertypeOf(it)} ($it)") }
println("--------------------------------------")
println("Non-Compatible Java types:")
//These should all print "false"
//This code doesn't work because the fields aren't valid Kotlin properties
JavaTestClass.NonCompatibleTypes::class.java.declaredFields.asSequence()
.map { it.kotlinProperty?.returnType }
.filterNotNull()
.forEach { println(" - ${parameterType.isSupertypeOf(it)} ($it)") }
}
Assuming the Type
comes from a declaration in Java, the following works for simple classes, parameterised types, raw types, and array types. There might be some edge cases that I didn't handle, though.
The idea is basically to check for each of the things that the Type
could be (Class
, ParameterizedType
, TypeVariable
, etc) and handle each case, and recursively traverse the "type tree" if needed.
fun convert(type: Type) = when(type) {
// simple arrays, simple classes, or raw types
is Class<*> -> when {
// we takeUnless it.isPrimitive here because primitve arrays are mapped to their own Kotlin classes, not Array<T>
type.isArray -> type.kotlin.createType(listOfNotNull(type.componentType?.takeUnless { it.isPrimitive }?.toKTypeProjection()))
else -> type.kotlin.createType(List(type.typeParameters.size) { KTypeProjection.STAR })
}
is ParameterizedType -> (type.rawType as Class<*>).kotlin.createType(
type.actualTypeArguments.map { it.toKTypeProjection() }
)
// arrays of parameterised types and type parameters
is GenericArrayType -> Array::class.createType(listOf(type.genericComponentType.toKTypeProjection()))
is TypeVariable<*> -> type.toKType()
else -> throw NotImplementedError()
}
fun Type.toKTypeProjection(): KTypeProjection = when (this) {
is WildcardType -> when {
// lowerBounds and upperBounds should contain at most one element in Java
lowerBounds.isNotEmpty() -> KTypeProjection(KVariance.IN, convert(lowerBounds[0]))
upperBounds.isNotEmpty() -> KTypeProjection(KVariance.OUT, convert(upperBounds[0]))
else -> KTypeProjection(KVariance.INVARIANT, convert(this))
}
else -> KTypeProjection(KVariance.INVARIANT, convert(this))
}
fun TypeVariable<*>.toKType() = when (val decl = this.genericDeclaration) {
// here we need to find the class/method that declares the type variable
is Class<*> -> decl.kotlin.typeParameters.first { it.name == this.name }.createType()
is Method -> decl.kotlinFunction!!.typeParameters.first { it.name == this.name }.createType()
else -> throw NotImplementedError()
}