Generate Java-Style Builders for Interop

We should be able to write Kotlin code that is reasonably usable from within Java. I think an excellent example of this is extension methods that are called from Java as static helpers with the receiver being the first argument. Kotlin should allow something similarly readable for builders.

I’m well aware we don’t need builders in Kotlin because we can use optional arguments. For example:

class Config(
    val domain: String,
    val port: Int = 443,
    val sslVerify: Boolean = true
)

Allows you to optionally specify the port and sslVerify, falling back to the default value if they do not exist.

Config("example.com", port = 80)

However, if you try to use Kotlin’s interop with Java and create a Config inside Java code, all of the default values get lost because everything is required. Even worse, everything is now by argument position rather than keyword, which makes the following really heard to understand without looking at the constructor for Config.

Config config = new Config("example.com", 123, false)

My suggestion is to have a @JvmBuilder annotation that functions like a stripped down version of Lombok’s @Builder annotation. Once applied, the Kotlin compiler should generate a Java-style inner Builder class. This will allow developers to choose where the right place for a Java-style builder is. If a particular code-set will have no Java at all, the developer can decide not to use @JvmBuilder compilation will proceed as it currently does. When the annotation is used, developers using the class can instantiate it the normal kotlin way (the builder won’t even be usable from Kotlin code) but Java developers will see the builder available to use.

Thoughts?

11 Likes

I should note that although the above example is contrived and small, this can quickly get out of hand when you have more parameters. I’ve got a class I’d like to convert to Kotlin that has 17 parameters and it’s very unwieldly to call from Java without a builder, but very nice to call from Kotlin.

2 Likes

In those cases is a constructor really the right solution (even with default parameters)? Remember that ll the parameters are passed to the actual constructor.

What else would you propose? It’s my understanding that using a constructor with a lot of default args is the most idiomatic way of replacing the builder pattern in Kotlin, and I’ve yet to see otherwise.

I should add that I’d like the final object to be immutable, so converting the vals to vars won’t work here.

2 Likes

I agree with the immutability. In my case I’m using builders. Those are also needed to create copies with arbitrary value changes. I agree it’s ugly. Perhaps generating builders in all cases could be a solution, although I feel that annotation processing (like Lombok) may be a better solution as it doesn’t tie Kotlin to a particular approach (do you copy values, or do you shadow them?).

As I understand it, Lombok cannot be used with Kotlin since it relies on a hack to actually generate into the same class that has the annotation (correct me if I’m wrong, I’d love to use Lombok here if possible). All other annotation processing solutions will produce a separate class (e.g., ConfigBuilder) outside of the built class (e.g., Config) unnecessarily creating a looser coupling than you’d expect.

Additionally, I really think there needs to be an in-language solution for this. I do not think it’s good practice that there is no way to use a constructor’s default value in Java.

1 Like

Any news on this? Would love to see something like that implemented as an annotation, like mike.holler proposed. I’m currently developing a Kotlin DSL which is basically unusable from Java because of that problem.

I’d also appreciate such a feature. Without it, much of the convenience of especially data classes is lost if being called from Java.

And with data classes, there is another issue: The copy(...) method. When being called from Java, you have to provide all the values, effectively making it useless. Maybe the @JvmBuilder annotation, or a separate one, could generate a copyBuilder() method that returns a builder, prefilled with the attribute values of the copied instance? Creating a copy with a single modified attribute would then look like myDataClass.copyBuilder().someAttribute("newValue").build().

3 Likes

If I understand the idea behind the compiler plugins right, this should be possible using a compiler plugin. Sadly they are still a feature without a stable or even public api. I think the JetBrains team mentioned a few times that they are planning to release them at some point but there is no official roadmap for this and I don’t think it has any priority.

1 Like

I wrote an @Builder annotation for Kotlin for exactly this purpose: GitHub - ThinkingLogic/kotlin-builder-annotation: A minimal viable replacement for the Lombok @Builder plugin for Kotlin code. It’s a standard kapt processor, so unlike Lombok doesn’t mess about with bytecode and therefore clients need to instantiate a new builder, e.g. new MyDataClassBuilder(), rather than invoking a static builder method on the class itself (MyDataClass.builder()). Otherwise, though, it’s pretty much a drop in replacement for the Lombok builder.

3 Likes

Cool, looks nice. Have you tried to check whether the class with the @Builder annotation has a companion object? You could add an extension to the companion which would return the builder. This however will only work if the class has a companion object already.

1 Like

Thanks :slight_smile: Nice idea. I hadn’t considered extension functions for companion objects! I’ll have a look when I get the chance (which may not be that soon).

Edit: Sadly it doesn’t seem possible to annotate extension functions with @JvmStatic, so Java classes wouldn’t see any such MyDataClass.builder() method. It would be visible in Kotlin, but you don’t really need it in Kotlin.

Would love native Kotlin support for something like this as an interop feature.

We created a KSP version of a builder generator: GitHub - beatgrid/kodeforge: Support for generation of Kotlin boilerplate code using KSP. It also supports default arguments in the constructor.

1 Like