I recently stumbled onto a subtle potential Kotlin problem. Let me play out the problem for you.
Imagine my code goes like this:
sealed class SuperClassOrchestrator(
open val medicineName: String,
protected val dummyField: Int = 0
){
fun orchestrate() {
println("SuperClassOrchestrator: ${this.medicineName}")
orchestrateComponent()
}
abstract fun orchestrateComponent()
}
data class SubClassComponent(override val medicineName: String) : SuperClassOrchestrator(medicineName+"__SUFFIX"){
override fun orchestrateComponent() {
println("SubClassComponent: $medicineName")
println("SubClassComponent: ${super.medicineName}")
}
}
If I try running this statement:
SubClassComponent("Complex Medicine Name").orchestrate()
it prints the following:
SuperClassOrchestrator: Complex Medicine Name
SubClassComponent: Complex Medicine Name
SubClassComponent: Complex Medicine Name__SUFFIX
This goes on to prove that Kotlin is maintaining two copies of common fields. The following decompiled class file supports that hypothesis even further. [ Removed unrelated code for convenience e.g. metadata/imports etc].
public abstract class SuperClassOrchestrator {
@NotNull
private final String medicineName;
private final int dummyField; //why is this always private? & not protected when explicitly mentioned?
private SuperClassOrchestrator(String medicineName, int dummyField) {
this.medicineName = medicineName;
this.dummyField = dummyField;
}
@NotNull
public String getMedicineName() {
return this.medicineName;
}
protected final int getDummyField() {
return this.dummyField;
}
public final void orchestrate() {
String str = "SuperClassOrchestrator: " + getMedicineName();
boolean bool = false;
System.out.println(str);
orchestrateComponent();
}
public abstract void orchestrateComponent();
}
& the decompiled subclass version goes like this:
public final class SubClassComponent extends SuperClassOrchestrator {
@NotNull
private final String medicineName; //why do we need this? It's supposed to be inherited
public SubClassComponent(@NotNull String medicineName) {
super(medicineName + "__SUFFIX", 0, 2, null);
this.medicineName = medicineName; //why do we need this? It's supposed to be inherited
}
@NotNull
public String getMedicineName() {
return this.medicineName;
}
public void orchestrateComponent() {
String str = "SubClassComponent: " + getMedicineName();
boolean bool = false;
System.out.println(str);
str = "SubClassComponent: " + super.getMedicineName();
bool = false;
System.out.println(str);
}
}
Why it did not sit well with me:
-
In subclass, even when I am explicitly telling override, Kotlin just ignores that anyways, & ends up creating new private field. It feels like oxymoron to me, override should not need creation of private fields.
-
If Kotlin ends up maintaining two copies of same field when it should not, it has a slim chance of inconsistency like shown in above example, which was never the case in Java/any OO languages.
-
Even if we declare a field as protected val e.g. in our superclass, Kotlin ends up creating private final anysways, but makes the getter protected!!! This feels counter-intuitive since code gives an impression of field overriding, what Kotlin ends up doing is Getter method overriding.
How it hurts us:
Any library we use, which is heavily reliant on reflection, won’t be able to discern the fact that Kotlin maintains copies of common fields. To give an example, when we tried this with Swagger, the common fields showed up in both Super class as well all Subclasses.
There’s another scenario where I can’t easily reason about the design choices. e.g. if we’re using Long, I was expecting Long wrapper class to appear in the decompiled version, but it didn’t.
Which is not the case if we use Long? in Kotlin. Then it falls backs to using Long wrapper class in decompiled version as depicted in above screen cap.
This leads to another interoperability problem, e.g. for an api contract in Spring boot
- accepts null value as well, even Kotlin type is Long which is not nullable
- Spring deserializer converts it to ZERO, since it heavily uses reflection & finds long primitive type instead of wrapper class [IMHO]
Now in these scenarios, the possible solutions could be using Kotlin flavor of 3rd party libraries or so. But that feels like half measure. Anyways, the answer I’m looking for are of the following questions:
What was reasoning behind these design choices [explained in above scenarios] ? What were the trade-offs for it? Was it a technical limitation which forced Kotlin to take that path?