The Difference between Kotlin’s Data Classes and Java 16 Records

The Difference between Kotlin’s Data Classes and Java 16 Records

A Detailed Look

When Java first introduced records as a preview in Java 14 and later released it with Java 16, it reminded many of Kotlin’s data classes. While they address the same problem in a very similar manner, there are a few differences in how they are handled.

I’ll list them roughly in the order of most relevant to more fringe. I tried to find all of the differences that exist, but if I missed something, feel free to comment about it.

Title photo by Steve Harvey on Unsplash

Examples & why they matter

Before getting into the differences, I want to show some examples. If you already know how they work, you can skip this section.

Java’s records use the new key word record:

record Name(String firstName, String lastName) { }

Meanwhile, Kotlin’s data classes use the modifier data in front of the class:

data class Name(val firstName: String, val lastName: String)

Both automatically create most of the boilerplate code that you’d otherwise have to create: getters, setters, equals, hashcode and toString.

This not only helps reduce boilerplate code, but can also make refactoring safer, since now you don’t have to remember to add a new field to the now auto-generated methods.

Final classes

Both records and data classes will compile to final classes. For Kotlin, this is the default for all classes, but data classes cannot be declared open.

Mutable members

Java’s records are completely immutable. With data classes, however, you can specify which fields are mutable and which are not (as usual with the var vs val keywords). For example, in the following code, id cannot be reassigned, but name can be:

record Person(val id: Int, var name: Name)

In the case of immutable fields, it will mark them as final in the resulting code. Java’s records always have final fields.

Non-constructor fields

Records do not allow you to define additional instance fields, only static ones. With data classes, however, you can. These are, of course, not included in the automatically generated methods, but can be useful for some eagerly created values:

data class Name(val firstName: String, val lastName: String) {
    val fullName = firstName + ' ' + lastName
}

java.lang.Record

Java’s records now always implement the new class [java.lang.Record](download.java.net/java/early_access/jdk17/d..). This abstract class doesn’t add any interesting methods by itself, but can be used to catch all records.

This super class also means that records cannot extend other classes — although they can still implement interfaces. Data classes do not have this restriction.

Destructuring

Kotlin already supports destructuring for data classes. For Java, there is currently a proposal floating around, but no current version of Java (including Java 17) supports this syntax.

Kotlin’s destructuring

Kotlin has destructuring built into it’s assignment syntax. Given the Person class from before, I can now do the following:

val (id, name) = Person(0, Name("Oscar", "Ablinger"))

This will be compiled to two different assignment statements utilizing another set of auto-generated methods. It’s essentially syntactic sugar for the following two statements:

val id = person.component1()
val name = person.component2()

These two are typed, so the resulting variables will update their types when changing the field types. They also don’t have to be exhaustive, so when you add another filed, then the same code will still work.

This, however, means that if you add a field as anything but the last element and it shares the type with the field it replaced, the code will compile and no error will be given! This a a very hard to track down source of bugs.

toString

While both get an auto-generated toString method, the results look slightly different for Java and Kotlin. Java will generate the following string:

record Name(String firstName, String lastName) { }

new Name("Oscar", "Ablinger").toString();
// Name[firstName=Oscar, lastName=Ablinger]

Kotlin generates a similar string:

data class Name(val firstName: String, val lastName: String)

Name("Oscar", "Ablinger").toString()
// Name(firstName=Oscar, lastName=Ablinger)

As you can see, the only real difference is that Java uses square brackets while Kotlin uses parenthesis. Annoyingly enough, the default toString that IntelliJ generates for classes uses braces.

You could also use this generated string as basis for your style guide for the toString method.

Reflection

Data classes are essentially just syntactic sugar over normal classes. Therefore, once compiled, it’s pretty much impossible to tell them apart from other classes.

Records however can be easily recognized using reflection. Not only because they always implement the java.lang.Record type, but also thanks to two new methods added to Class: [isRecord](download.java.net/java/early_access/jdk17/d..) and [getRecordComponents](download.java.net/java/early_access/jdk17/d..).

isRecord simply returns true if the class is a record and false otherwise.

getRecordComponents returns information about all of the fields of the record or null if it’s not a record. The term “component” is simply used to refer to the non-static fields of a record. Using this you have access to the same information that you’d expect from methods: annotations, name, type and more.

Getters

Kotlin generates getters after the well-known pattern with get prefix. Records now dropped this prefix and instead name the methods the same as the field.

Implementations

I already wrote about the different results of the toString methods. Additionally, the implementation of all of the methods is also done differently.

While the Kotlin compiler simply generates the code for each of the methods, Java outsources the actual code to a static method. The implementation of those methods simply call the [ObjectMethods.bootstrap](download.java.net/java/early_access/jdk17/d..) method with the necessary information. I assume this is done to reduce the byte size of records as much as possible.

Conclusion

In the end, data classes and records are pretty similar in their usage. But while data classes are simply syntactic sugar, records add a lot to the Java standard library. Nonetheless, data classes do allow for more features than records. Whether that’s a strictly good thing is a different debate.