2

We have two projects, and the kotlin one publishes a package that's imported by java.

In kotlin, is a value class like

@JvmInline
value class CountryId(private val id: UUID) {
    override fun toString(): String = id.toString()
    companion object { fun empty(): CountryId = CountryId(EMPTY_UUID) }
}

In java, we can't see a constructor, or actually instantiate this class. I have also tried creating a factory in Kotlin to create them

class IdentifierFactory 
{
    companion object {
        fun buildString(): String {
            return "hello"
        }

        fun buildCountry(): CountryId {
            return CountryId.empty()
        }
    }
}

In java, I can call IdentifierFactory.Companion.buildString() and it will work, but IdentifierFactory.Companion.buildCountry() doesn't even exist.

Is Java really this awful with Value classes?

ps. I've attempted with @JvmStatic as well, with no success

pps. If I decompile the kotlin bytecode from the java side, and get a CountryId.decompiled.java, this is what the constructor looks like

// $FF: synthetic method
private CountryId(UUID id) {
    Intrinsics.checkNotNullParameter(id, "id");
    super();
    this.id = id;
}

ppps. Kotlin 1.5.21 and Java 12

CaffGeek
  • 21,856
  • 17
  • 100
  • 184
  • This is likely due to name mangling with value classes. Kotlin does that to avoid overload conflicts on Java side. You might be able to get around it with `@JvmName` to customize the Java method name – Joffrey Nov 10 '21 at 22:11
  • oh... I think that might solve it! – CaffGeek Nov 10 '21 at 22:20
  • Well...kinda... I still don't end up with a "CountryId" I have a UUID... which isn't the type I want... ideally, I'd just like the constructor to work and not use the factory (which doesn't work anyhow) – CaffGeek Nov 10 '21 at 22:24
  • I'm confused now, the regular `CountryId` constructor works fine for me from Java. What do you mean it doesn't work for you? – Joffrey Nov 10 '21 at 22:39
  • It expects 0 arguments in java for some reason. Looks like it's marked the constructor as private? – CaffGeek Nov 10 '21 at 22:42
  • Maybe this has to do with how the project is published / imported, but it shouldn't really. In my case I'm doing my tests with both the Kotlin and the Java class in the same project (under `src/main/kotlin` and `src/main/java` respectively). – Joffrey Nov 10 '21 at 22:45
  • We're publishing a jar to maven, and pulling it in on the java side. – CaffGeek Nov 10 '21 at 22:48
  • I am not able to find the IdentifierFactory class. Getting Error: Cannot resolve symbol 'IdentifierFactory'. Can you tell me to which package this belongs? Or if I am missing anything? – Saurabh Padwekar Mar 07 '23 at 14:28

1 Answers1

6

Is Java really this awful with Value classes?

Value classes are a Kotlin feature. They are basically sugar to allow more type safety (in Kotlin!) while reducing allocations by unboxing the inner value. The fact that the CountryId class exists in the bytecode is mostly because some instances need to be boxed in some cases (when used as a generic type, or a supertype, or a nullable type - in short, somewhat like primitives). But technically it's not really meant to be used from the Java side of things.

In java, I can call IdentifierFactory.Companion.buildString() and it will work, but IdentifierFactory.Companion.buildCountry() doesn't even exist.

The functions with value classes in their signature are intentionally not visible from Java by default, in order to avoid strange issues with overloads in Java. This is accomplished via name mangling. You can override the name for the Java method by using the @JvmName annotation on the factory function on Kotlin side to make it visible from Java:

@JvmName("buildCountryUUID") // prevents mangling due to value class
fun buildCountry(): CountryId {
    return CountryId.empty()
}

Then it is accessible on Java side and returns a UUID (the inlined value):

UUID uuid = IdentifierFactory.Companion.buildCountryUUID();

ideally, I'd just like the constructor to work and not use the factory

I realized from the comments that you were after creating actual CountryId instances from Java. Using the CountryId constructor from Java works fine for me:

CountryId country = new CountryId(UUID.randomUUID());

But I am not sure how this is possible, given that the generated constructor is private in the bytecode...

Joffrey
  • 32,348
  • 6
  • 68
  • 100
  • Yep, the factory worked as you stated... but I'm really trying to create a variable of "CountryId" in Java, using the kotlin class ... not sure why the constructor isn't working. If I go into the bytecode (from the java side) and tell it to decompile, I see it's a private constructor (see update to initial post) – CaffGeek Nov 10 '21 at 22:46
  • @CaffGeek it's strange that it's private for you. In my case the constructor with `UUID` is definitely accessible. Have you tried using the constructor from a Java class in the Kotlin project, just to test it? – Joffrey Nov 10 '21 at 22:48
  • We have nothing going in that direction right now. The java app is our legacy stuff, and it's consuming new services and these id types from our kotlin projects. – CaffGeek Nov 10 '21 at 22:55
  • I wonder if it's related to versions? We're on Kotlin 1.5.21 and Java 12 (I "think" an upgrade to 17 is in the works...) – CaffGeek Nov 10 '21 at 23:05
  • I'm using 1.5.31 for my tests here, and JDK 11 – Joffrey Nov 10 '21 at 23:12
  • 1
    I examined the bytecode for the `CountryId` class on my side, and I also get a private constructor there. I have no idea why this constructor can successfully be called from my Java class – Joffrey Nov 10 '21 at 23:27
  • haha, weird. I updated to 1.5.31, and still have a private constructor as well... weird. – CaffGeek Nov 10 '21 at 23:28
  • any idea what's going on, why you can call that private constructor but I can't? – CaffGeek Nov 15 '21 at 14:53
  • 1
    if I put it in another project beside the java one, it seems to work... the issue seems to be when brining it in from a jar via maven – CaffGeek Nov 15 '21 at 15:13
  • @CaffGeek I tried to actually run this stuff and I do get a runtime exception (`Cannot find symbol`) when trying to call the constructor. So I guess it might be an IDE bug? – Joffrey Nov 15 '21 at 16:14
  • interesting. Looks like this simply isn't going to work then I guess :( – CaffGeek Nov 15 '21 at 16:16