One of the greatest strong points of Kotlin can also be its weakness. When we talk about data classes today, and in the same way java records, we tend to focus, at least many people I know do, on the complete elimination of the boilerplate code. Boilerplate code has been something that has been annoying developers for ages. It exists not only in Java, but other languages have that as well. If we think about creating data access objects any way, it doesn’t really matter if we do it in Kotlin or Java or any other language. Frequently we repeat the same process: repository creation, service and controller. A lot of what we do is boilerplate any way still. However boilerplate code may have been a part of the decision to create data classes or java records, a more important paradigm of software engineering was the actual focal point of creating these types of data structures. It all boils down to immutability or in any case the possibility thereof.
Let’s rollback the time and go back to 1995 where we started at some point to create data structures where we would specify different things. We would define the fields, the getters, the setters, the properties, the accessors, the attributes and the parameters in a way to be able to pass our arguments to either the methods, the functions or the contructors.
Fot this part we will have some fun programming using the characters of the mid-80’s series: The Golden Girls. Having said this, let’s see how a class would look like for about 15 years:
public class GoldenGirlsJava {
public String goldenGirl1;
private final String goldenGirl2;
private final String goldenGirl3;
private final String goldenGirl4;
public GoldenGirlsJava() {
this.goldenGirl1 = "Dorothy Zbornak";
this.goldenGirl2 = "Rose Nylund";
this.goldenGirl3 = "Blanche Devereaux";
this.goldenGirl4 = "Sophia Petrillo";
}
public GoldenGirlsJava(
String goldenGirl1,
String goldenGirl2,
String goldenGirl3,
String goldenGirl4
) {
this.goldenGirl1 = goldenGirl1;
this.goldenGirl2 = goldenGirl2;
this.goldenGirl3 = goldenGirl3;
this.goldenGirl4 = goldenGirl4;
}
public String getGoldenGirl1() {
return goldenGirl1;
}
public void setGoldenGirl1(String goldenGirl1) {
this.goldenGirl1 = goldenGirl1;
}
public String getGoldenGirl2() {
return goldenGirl2;
}
public String getGoldenGirl3() {
return goldenGirl3;
}
public String getGoldenGirl4() {
return goldenGirl4;
}
@Override
public String toString() {
return "GoldenGirlsJava{" +
"goldenGirl1='" + goldenGirl1 + '\'' +
", goldenGirl2='" + goldenGirl2 + '\'' +
", goldenGirl3='" + goldenGirl3 + '\'' +
", goldenGirl4='" + goldenGirl4 + '\'' +
'}';
}
}
This was a tremendous amount of code to type in only to create two contructors. Although the hashcode and equals weren’t necessarily a must in all occasions, we almost always had to implememt a no arguments contructor next to a more generalistic constructor. The good thing about creating all of this boilerplate code is that we knew very clearly how everything would look like after compiling to bytecode. However, and in any case, boilerplate code was always a contentious issue for many developers and so in 2009, lombok came along and revolutionized the way we create data structures. It introduced the concept of using an annotation processor and interpreting specific annotations that would give the qualities we needed for our classes and so a lombok annotated class would look like this:
@Getter
@Setter
@AllArgsConstructor
@ToString
public class GoldenGirlsLombok {
public String goldenGirl1;
private final String goldenGirl2;
private final String goldenGirl3;
private final String goldenGirl4;
public GoldenGirlsLombok() {
this.goldenGirl1 = "Dorothy Zbornak";
this.goldenGirl2 = "Rose Nylund";
this.goldenGirl3 = "Blanche Devereaux";
this.goldenGirl4 = "Sophia Petrillo";
}
}
And for a while, people were very happy about Lombok! Finally the weight of having to create all of that boilerplate code was gone. But that felling endured for about 7 years, when a new player arrived and this time it was something the software development industry didn’t expected. It was called Kotlin and in 2016 it debuted with the immediate introduction of data classes. Our golden girls implementation in Kotlin would now look like this:
data class GoldenGirls(
var goldenGirl1: String = "Dorothy Zbornak",
private val goldenGirl2: String = "Rose Nylund",
private val goldenGirl3: String = "Blanche Devereaux",
private val goldenGirl4: String = "Sophia Petrillo"
)
Although Kotlin slowly started gathering fans, Java on the other hand realized that something else was in the market and some developments on that side started gaining some steam like project Loom which had been in the making but also left in the back burner for a while. That is why, with the release of Java 14 in 20202, Java introduced java records, and data structures in Java would now look like this:
public record GoldenGirlsRecord(
String goldenGirl1,
String goldenGirl2,
String goldenGirl3,
String goldenGirl4
) {
public GoldenGirlsRecord() {
this(
"Dorothy Zbornak",
"Rose Nylund",
"Blanche Devereaux",
"Sophia Petrillo"
);
}
}
And to this day code simplification and code reduction seems only to continue. However with the reduction of the boilerplate code, the concepts of fields, getters, setters, properties, accessors, attributes and parameters became far less visual and less easy to map in our minds. Whether we like it or not those concepts are still how the JVM works and organizes code with the bytecode.
But code oversimplification is really about making code easier to read and in the case of data classes and java records the idea is also to create data structures that are immutable or partially immutable. Java records are truly immutable in the sense that all values it contains or references it contains cannot be modified. Kotlin data classes can be also truly immutable for the same reasons but they don’t have to which in a way gives permission to developers to create complicated and worst of all, mutable code anyway.
In any case this is all good until we then need to work with frameworks like the Spring Framework, which rely heavily on annotations. Here is an example:
@Entity
@Table(name = "shelf_case")
data class Case(
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
val id: Long?,
var designation: String?,
var weight: Long?
) {
constructor() : this(0L, null, null)
override fun toString(): String {
return super.toString()
}
}
This example works fine. To the unsuspecting eye, there isn’t a lot special going on here. This works in Kotlin, just in the same way as it would work in Java. But now let’s have a look at a similar example, but this time with Jackson annotations:
data class AccountNumbersPassiveDto(
@NotNull
val accountNumberLong: Long?,
val accountNumberNullable: Long?,
@DecimalMax(value = "10")
@DecimalMin(value = "5")
val accountNumber: BigDecimal,
val accountNumberEven: Int,
val accountNumberOdd: Int,
@Positive
val accountNumberPositive: Int,
@Negative
val accountNumberNegative: Int,
@DecimalMax(value = "5", groups = [LowProfile::class])
@DecimalMax(value = "10", groups = [MiddleProfile::class])
@DecimalMax(value = "15", groups = [HighProfile::class])
@DecimalMax(value = "20")
val accountNumberMaxList:Int
)
This example will fail and the reason for that is that in Kotlin, there is no way to tell the compiler where exactly to we want our annotation to be applied to. In the previous example with annotation @Entity
, which is a jakarta persistence annotation, the annoations are applied correctly to the fields. If we decompile that code we’ll find this:
public final class Case {
@Id
@GeneratedValue(
strategy = GenerationType.SEQUENCE
)
@Nullable
private final Long id;
@Nullable
private String designation;
@Nullable
private Long weight;
But for the latter, which is a jakarta validation example, we find this:
public final class AccountNumbersPassiveDto {
@Nullable
private final Long accountNumberLong;
@Nullable
private final Long accountNumberNullable;
@NotNull
private final BigDecimal accountNumber;
private final int accountNumberEven;
private final int accountNumberOdd;
This means that AccountNumbersDto has not been affected by those annotations on its fields. We were just lucky in the first example. It might have actually also failed.
In order to specify where our annotation needs to go to, Kotlin gives us the possibility to use use-site targets as prefix to any annotation. The requirement is of course that conditions are met. In this case, one way to fix this is to prefix all of the annotations with @field
like this:
data class AccountNumbersPassiveDto(
@field:NotNull
val accountNumberLong: Long?,
val accountNumberNullable: Long?,
@field:DecimalMax(value = "10")
@field:DecimalMin(value = "5")
val accountNumber: BigDecimal,
val accountNumberEven: Int,
val accountNumberOdd: Int,
@field:Positive
val accountNumberPositive: Int,
@field:Negative
val accountNumberNegative: Int,
@field:DecimalMax(value = "5", groups = [LowProfile::class])
@field:DecimalMax(value = "10", groups = [MiddleProfile::class])
@field:DecimalMax(value = "15", groups = [HighProfile::class])
@field:DecimalMax(value = "20")
val accountNumberMaxList:Int
)
If we now try to decompile the resulting bytecode using IntelliJ, we’ll find that it results in this code in Java:
public final class AccountNumbersPassiveDto {
@NotNull
@Nullable
private final Long accountNumberLong;
@Nullable
private final Long accountNumberNullable;
@DecimalMax("10")
@DecimalMin("5")
@org.jetbrains.annotations.NotNull
This means that the annotations have been applied to field and that our code works as it is supposed to.
You can probably imagine at this point that people who have been working on Java for a long time, will have no problem in adapting to Kotlin because we all know what fields, parameters, properties, etc, are. What we observe today and I witness that too, is that it seems like the code simplification has a potential to backfire. There have been quite a few occasions where I have become aware of the time spent in projects trying to figure how why in some projects or modules, the annotations just don’t seem to work. And it all seems to boil down to not making use of the use-site targets. My theory on that is that most likely, new generations of developers will look at things that my generation didn’t think was complicated and learned out of instinct, as really complicated material. Such is the case with use-site targets. These used to be things that we could very easily visualize. However in current times, the generated confusion with this seems to be delaying projects from being developed on time in some occasions. I wouldn’t be able to generalize of course but there is potential for good and there is potential for bad with java records and data classes. How data classes and records will shape our future is hard to tell, but one thing that is clear is that because of this problematic, other frameworks are betting on going completely annotation free as is the case with Ktor.
I have created documentation on this on Scribed:
Fields-in-Java-and-Kotlin-and-What-to-Expectand also on slide-share:
fields-in-java-and-kotlin-and-what-to-expect.
You can also find the examples I’ve given on GitHub. The golden girls example can be found here:jeorg-kotlin-test-drives and the jakarta persistence and validation annotations examples can be found here:
[jeorg-spring-master-test-drives][https://github.com/jesperancinha/jeorg-spring-master-test-drives).
Finally I also created a video about this on YouTube that you can have a look at right over here:
Have a good one everyone!
Top comments (0)