Search code examples
kotlinkotlin-reflect

Kotlin: implicit conversion from Int to nullable Double fails in reflection


I was trying to convert a Map<String, Any> to data class instance:

data class Person(
    val name: String,
    val age: Int,
    val weight: Double?,
)

fun test() {
    val map = mapOf(
        "name" to "steven",
        "age" to 30,
        "weight" to 60,
    )
    
    val ctor = Person::class.constructors.first();
    val params = ctor.parameters.associateWith {
        map[it.name]
    }
        
    val instance = ctor.callBy(params)
    println(instance)
}

The code above throws java.lang.IllegalArgumentException: argument type mismatch because 60 is passed to weight as an Int, and kotlin does not support implicit conversion.

Then I change the type of weight to non-nullable Double

data class Person(
    val name: String,
    val age: Int,
    val weight: Double, // !
)

and that works, even 60F works too.

My question is:

  1. Why implicit conversion works only when type is non-nullable?

  2. How to do implicit conversion when type is nullable?


Solution

  • Assuming this is Kotlin/JVM...

    After digging a bit into how callBy is implemented on the JVM, I found that it eventually calls Constructor.newInstance. The documentation for that says:

    Individual parameters are automatically unwrapped to match primitive formal parameters, and both primitive and reference parameters are subject to method invocation conversions as necessary.

    When you use the non-nullable Double in Kotlin as a parameter type, it is translated to the primitive type double in the JVM world. However, if you use the nullable type Double?, it is translated to the reference type wrapper java.lang.Double. This is because the primitive type double cannot be null. A similar thing happens for Int and Int?.

    The "method invocation conversions" that the newInstance docs mentioned (which I think is referring to the list of conversions allowed in an invocation context) does not include a conversion from int to java.lang.Double. After all, this Java code does not compile:

    public static void foo(Double d) {}
    
    // ...
    
    int x = 1;
    foo(x)l
    

    But it would have compiled if foo had taken the primitive double.

    As for your second question, I can't think of anything better than just checking each argument and parameter type manually, and performing the conversion explicitly for each case.

    If you can change Person, I'd suggest adding a secondary constructor that takes a non-nullable Double, and change the calling code to choose an appropriate constructor.

    Or if you really don't like choosing constructors, change the existing one to take Number?, and convert it to an appropriate numeric type using the toXXX methods before using it.