Immutability in Java records vs Kotlin data classes

Vivek Malhotra
4 min readOct 20, 2023

--

Introduction

Every Java developer from early 2000s who has written a lot of data classes would have agreed that we had to write a lot of repetitive code just to implement the basic necessities expected from data classes. A simple data class in Java prior to jdk 14 was written somewhat as:

public class Person {
public final String name;
public final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}

@Override
public int hashCode() {
return Objects.hash(name, age);
}

@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

Though data classes were around in Koltin 1.0 since 2016 not everyone Java developer could leverage that. Firstly you either needed to write and compile Kotlin code along side Java in your commercial Project or also seek a vote in the team to even make a move towards Kotlin.

Records were introduced in Java 16 as a feature in 2021 after the JEP 395. They were proposed with similar intent to help developers create simple and dumb data classes. However the JEP also talks about carrier of immutable data. And this is what made me write this article.

Kotlin data classes

Kotlin was the first to introduce compact way to write data classes. The main purpose was to generate initialisation in canonical constructor, equals(), hashCode() and toString(). The kotlin version of your Person data class in Kotlin will be as concise as:

data class Person(val name: String, val age: Int)

This was a leap forward and saved so much time writing a simple data class. Not just this kotlin also

  • generates copy constructors
  • lets you define default values for the fields in your data class

Java Records

Records when introduced in Java was a breath of fresh air. Like finally we have something like kotlin data classes. So a record would look like

public record Person(String name, int age) {

}

And this is good concise declaration of a data class.

Java Records vs Kotlin data classes

Under the hood Java records does a lot of things like Kotlin data class.

  • Auto generated equals(), hashCode(), toString().
  • initialization of fields in constructor

What Java records does not do and makes it fall short to Kotlin are

  • copy constructors
  • allow default values to fields

When we started writing backend applications in Kotlin we felt these features of data classes in kotlin really helped speed up our development time. However there is one thing which Java records are good at is enforcing immutability.

Shallow immutability in Kotlin data classes

Kotlin data classes only enforce shallow immutability and it is harded to enforce full immutability pattern on them. An example is let us we have a class Train which has a Set of stations:

data class Train(val name: String, val stations: List<String>)

This contruct provides shallow immutability so if you instantiate a train instance with a mutable list of stations like

fun main(){
val stations = mutableListOf("Mumbai")
val train = Train(name="TR01", stations = stations)
stations.add("Pune")
}

You can modify the stations of train TR01. And Kotlin does not have a way to create a copy read only list in Train during initialization.

Immutability in Java records

Records are more versatile and immutability is at its core. You can enforce immutability pattern in Java for the same train class as

public record Train(String name, List<String> stations) {
public Train(String name, List<String> stations) {
this.name = name;
this.stations = List.copyOf(stations);
}
}

So when you initialise a Train instance with a name and list of stations you cannot modify the stations of the train.

How to enforce Immutability in Kotlin ?

Currently there are two ways of enforcing immutability.

(updated 20 Nov 2023 based on feedback) Firstly you can write your data class with a private param _station to primary constructor and declare/initialize station field inside the body.


data class Train(val name: String, private val _stations: List<String>) {
val stations = _stations.toList()
}

The second way I see right now to write a deep immutable class is not a data class at all. You can do it in a Kotlin class as

class Train(val name: String, stations: List<String>) {
val stations = stations.toList()
}

And this then brings you to classes without any synthetic equals() and hashCode().

Conclusion

Even though Kotlin data classes are powerful and versatile than Java records because of the copy constructors and default values but they enforce shallow immutability to the class fields.

Do comment if you like this article or if I missed an approach while writing it and I would update it promptly.

--

--

Vivek Malhotra

consider myself an average Java developer. My recent interest has been asyn and non-blocking frameworks based on Java and Kotlin