Java reflection and Kotlin primitive types

IDEA warns me that Java boxed types shouldn’t be used, and that I should use respective Kotlin types instead. It works most of the time, but sometimes leads to unexpected results. Here’s an example.

class Property<T> private constructor(val type: Class<T>) {
    companion object {
        @JvmField val STRING_PROPERTY = Property(String::class.java)
        @JvmField val INT_PROPERTY = Property(Int::class.java)
        @JvmField val LONG_PROPERTY = Property(Long::class.java)
    }
}

class ClassWithProperties {
    private val properties = HashMap<Property<*>, Any>()

    fun <T : Any> setProperty(property: Property<T>, value: T) {
        properties[property] = value
    }

    fun <T : Any> getProperty(property: Property<T>): T = property.type.cast(properties[property])
}

This compiles, as well as the following Java code:

        ClassWithProperties o = new ClassWithProperties();
        o.setProperty(Property.STRING_PROPERTY, "string");
        o.setProperty(Property.INT_PROPERTY, 12345);
        o.setProperty(Property.LONG_PROPERTY, 12345L);
        o.getProperty(Property.STRING_PROPERTY);
        o.getProperty(Property.INT_PROPERTY);
        o.getProperty(Property.LONG_PROPERTY);

However, it throws at runtime: java.lang.ClassCastException: Cannot cast java.lang.Integer to int. Well, that’s expected because I use Java reflection to cast and Java knows nothing of Kotlin types. The equivalent code in Kotlin throws the same exception, no surprise here too. To make it work, I can change the code like this:

class Property<T> private constructor(val type: Class<T>) {
    companion object {
        @JvmField val STRING_PROPERTY = Property(String::class.java)
        @JvmField val INT_PROPERTY = Property(Integer::class.java)
        @JvmField val LONG_PROPERTY = Property(java.lang.Long::class.java)
    }
}

Now it works just fine, but looks ugly enough already. The calling code looks even more ugly in Kotlin:

        val o = ClassWithProperties()
        o.setProperty(Property.STRING_PROPERTY, "string")
        o.setProperty(Property.INT_PROPERTY, 12345 as Integer)
        o.setProperty(Property.LONG_PROPERTY, 12345L as java.lang.Long)
        o.getProperty(Property.STRING_PROPERTY)
        o.getProperty(Property.INT_PROPERTY)
        o.getProperty(Property.LONG_PROPERTY)

This, of course, generates warnings about Java boxed classes used in Kotlin. I can just suppress them, but still. Java code, on the other side, works just fine without any warnings or errors, just as expected.

Is this an unavoidable problem with Java interop? Of course, if generics in Java used reified types to begin with, like .Net does, it wouldn’t be a problem at all. But since it’s not the case, I’m trying to emulate reified types with reflection. Should I just suppress those warnings and bear with it? Is it possible to do the same thing in a more elegant way?

You can use Long::class.javaObjectType. It will return the Class object representing the wrapper class, java.lang.Long in this case.

https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.jvm/java-object-type.html

1 Like

Very interesting. What exactly is Int::class.javaObjectType? It returns a value of type Class<Int>, so that is essentially Integer.class cast to Class<Int>. This way we get runtime type as Integer, but compile-time type ad Int. Any undesirable side effects from such a trick?

Kotlin’s primitive types do not exist at runtime. Kotlin type Int is represented as JVM int in primitive contexts (essentially everywhere it’s allowed), and as java.lang.Integer everywhere else. And when translating JVM types back to Kotlin, both java.lang.Integer and int map to kotlin.Int.

In practice, I’d say it never causes any problems because of the autoboxing conversions present both in Kotlin and Java, and the lack of primitive specialization on JVM. Any method you can call on Class<Int> already takes or returns an erased type Object instead of primitive int. So the only confusing bit is having two objects of type Class<Int> which are not equal to each other; but that is IMHO kind of similar to having any two Class objects of the same type loaded by different class loaders.

Wait, how can they be not equal if it’s the same type at runtime? At least Integer::class.java==Int::class.javaObjectType evaluates to true, which is no surprise since javaObjectType is actually implemented as a simple unchecked cast.

Sorry if my wording is confusing, I meant the following two objects:

  1. Class object for java.lang.Integer, which is normally accessible via Int::class.javaObjectType in Kotlin, and Integer.class in Java.
  2. Class object for the primitive integer type, which is normally accessible via Int::class.java (or Int::class.javaPrimitiveType) in Kotlin, and int.class (or Integer.TYPE) in Java.

These two Class objects are not equal because they represent different classes, namely the wrapper type and the primitive type.

The syntax that you mention, Integer::class.java, is getting the same underlying Class object for the wrapper type java.lang.Integer, so it’s of course equal to Int::class.javaObjectType. But it kind of exposes how primitive types are implemented internally in Kotlin and thus is not recommended.

Ah, that makes sense. But my previous question wasn’t about Int::class.java vs Int::class.javaObjectType, but rather about Int::class.javaObjectType itself. It is statically typed as Class<Int>, but at runtime it corresponds to Integer.class (in Java syntax), which normally has the static type Class<Integer>. Won’t that cause any trouble?

I don’t think it would cause any trouble. The mapping of Kotlin<->JVM types is a pretty fundamental concept in the Kotlin compiler, and it has been tested for quite some time already. Feel free to prove me wrong though! :smiley:

From a user’s point of view, it makes sense to just forget about the internal implementation details (i.e. that kotlin.Int is backed by java.lang.Integer) and only use Kotlin types everywhere. This type mapping should be basically unnoticeable.

That’s exactly what I’d expect. And that’s why I was surprised to see that the code in my first post (the Int::class.java variant) compiled fine and without any single warning and then failed at runtime. What’s funny, it fails at getProperty(), that is, it’s perfectly fine to store an int, but impossible to get it back because Integer cannot be cast to Int.

Come to think of, it… why? If Int is a Kotlin (not Java) class that corresponds to Java’s int, then why Class<Int>.cast() doesn’t work for values of type Integer? Is it just the way Java type system works? That is, Integer is not a subtype of Int, and that’s why the cast is impossible? And there’s no way to override that behavior because Class is still a type from Java standard library that works the way Java tells it to and there is no way to force it to be a little more gentle about such cross-language casts?

Or is it possible, just wasn’t done in Kotlin for some reason? For example, it would have undesirable side effects or break some important principles?

As I mentioned, in reality the class kotlin.Int does not exist (there’s literally no such class file in kotlin-stdlib), it’s represented by Java types int and java.lang.Integer at runtime. And the semantic difference between these two types is usually unnoticeable, unless you’re using Java reflection. These types are incompatible in the JVM, so any problems in this regard indeed have to do with JVM itself – nothing Kotlin can do here. So the answer to

why Class<Int>.cast() doesn’t work for values of type Integer ?

would be the same as the answer to the question: “why doesn’t Integer.TYPE.cast(42) work in Java?” It throws ClassCastException because the value 42 is boxed to java.lang.Integer, which is incompatible with int. Which is, I admit, very confusing. Kotlin kind of makes it look like it will succeed by using unified syntax for primitive and wrapper types, but of course it’s still working on JVM, which has this limitation that we can’t circumvent ¯\(ツ)/¯.

Oh, wait, what, Integer.TYPE in Java is not the same as Integer.class?! Now I see. I’ve been programming in Java for more than 15 years and never ever had to use Java classes for primitive types. Never even knew Java had them! Which is no surprise given that Integer.TYPE is declared as Class<Integer>, even though it does not correspond to Integer.

So let me sum it up:

  1. Int is purely a compile-level abstraction, at least as far as the JVM is concerned. There is no such class (as in a *.class file).
  2. Int is represented as int if possible, as Integer otherwise. It’s autoboxing, implemented, unlike Java, not as a language feature, but as a compile-time optimization.
  3. Int::class.java is the same as Integer.TYPE in Java. In Kotlin Int::class.java == Integer.TYPE evaluates to true.
  4. Int::class.javaObjectType is the same as Integer.class in Java. In Kotlin, Int::class.javaObjectType == Integer::class.java evaluates to true.
  5. Java reflection only works with types that inherit Object, that is, not primitive types (obvious), and therefore it only works with Int::class.javaObjectType (not so obvious unless the above points are well understood).
2 Likes