javakotlinmavenlanguage-interoperabilitykotlin-inline-class

Kotlin interoperability issue with implementing interfaces


I'm working on migrating a web project from Java to Kotlin. During this process I'm faced with compilation issue that can make problems in the future, especially where the existing projects have to reuse Kotlin components.

The issue I localized and reproduced in simple way:

Kotlin version: 1.9.23

This is EntityId which is an inline value class

@JvmInline
value class EntityId(val id: UUID)

and Kotlin interface that contains abstract function with EntityId

interface KotlinInterface<T> {

    fun findById(id: EntityId): T
}

I try to implement this interface in Java (since Java doesn't support Kotlin value classes, the compiler expects UUID type instead of EntityId):

public class JavaImpl implements KotlinInterface<Entity> {

    @Override
    public Entity findById(@NotNull UUID id) {
        return new Entity(id, "Some name");
    }
}

The IDE doesn't see any issues and no errors are reported.

I try to compile project using Maven and get the following:

[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  22.602 s
[INFO] Finished at: 2024-07-06T12:48:51+04:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.11.0:compile (compile) on project kotlin-interpolation: Compilation failure: Compilation failure: 
[ERROR] .../kotlin-interpolation/src/main/kotlin/com/example/JavaImpl.java:[9,8] com.example.JavaImpl is not abstract and does not override abstract method findById-WQTzO-4(java.util.UUID) in org.example.com.example.KotlinInterface
[ERROR] .../kotlin-interpolation/src/main/kotlin/com/example/JavaImpl.java:[11,5] method does not override or implement a method from a supertype
[ERROR] .../kotlin-interpolation/src/main/kotlin/com/example/JavaImpl.java:[13,16] Entity(java.util.UUID,java.lang.String) has private access in org.example.com.example.Entity
[ERROR] -> [Help 1]

The project for reproducing issue you can find here: github: Kotlin-interpolation issue

Is there the way to avoid such issue in correct way? (Creating bridges, adapters or proxy is not an option.)

Code/configuration examples are encouraged.

UPD1: Thanks to somethingsomething I could avoid the issue by adding annotation in the interface:

interface KotlinInterface<T> {

    @Suppress("INAPPLICABLE_JVM_NAME")
    @JvmName("findByIdForJava")
    fun findById(id: EntityId): T
}

and implemented in java the findByIdForJava(...):

public class JavaImpl implements KotlinInterface<Entity> {

    @Override
    public Entity findByIdForJava(@NotNull UUID id) {
        return new Entity(id, "Some name");
    }
}

BUT, as somethingsomething mentioned below we faced with compile issue for Entity, that's why I came up with workaround where I added static constructor for :

data class Entity(val id: EntityId, val name: String) {
    companion object {
        @JvmStatic
        fun createCompatible(id: UUID, name: String): Entity {
            return Entity(EntityId(id), name)
        }
    }
}

and use it in the Java implementation:

    @Override
    public Entity findByIdForJava(@NotNull UUID id) {
        return Entity.createCompatible(id, "Some name");
    }

This option looks a bit messy, but hacked the interoperability issue.

May be the are other options to do fix it in proper way?


Solution

  • Your reported issue is just caused by name mangling of value classes, this is both explained in the kotlin docs (https://kotlinlang.org/docs/inline-classes.html#mangling) and in the related question that was linked in the comments.

    You can just do something like this:

    @Suppress("INAPPLICABLE_JVM_NAME")
    @JvmName("foo")
    fun findById(id: EntityId): T
    

    And in you java class implement the method foo(...) instead.

    After doing this I however end up with a new issue:

     return new Entity(id, "Some name");
    

    fails with [ERROR] /tmp/kotlin-interpolation/src/main/kotlin/com/example/JavaImpl.java:[13,20] Entity(java.util.UUID,java.lang.String) has private access in org.example.com.example.Entity

    Looking at the decompiled class we can see 2 constructors:

    private Entity(UUID id, String name)
     // $FF: synthetic method
    public Entity(UUID id, String name, DefaultConstructorMarker $constructor_marker)
    

    Replacing EntityId with UUID in the Entity class and recompiling results in the same constructors, except the first one is now public.

    So it looks to me like some undocumented (as far as I can find) behaviour related to value classes in public constructors, where the constructor becomes private, which is going to block your code from working when called from java.