Java Records and Kotlin Data Classes

Java 14 comes with a new language feature called Records. Records make developers life easier by adding all the required code when dealing with data classes. Kotlin provides similar feature (data classes) and this article will highlight the similarities and the differences between them.

We frequently create classes whose main purpose is to hold data. In such a class some standard functionality and utility functions are often mechanically derivable from the data.
In Kotlin, this is called a data class

Data classes are declared by using the data keyword right before class:

data class Point(val x: Int, val y: Int)

The compiler automatically derives the following methods based on the declared properties:
equals()/hashCode()
toString()
copy()

So far it’s similar to what Java Records have except for the copy method. It’s a convenience method that allows to copy the object while altering some of its properties. Consider the following example:

data class Point (val x: Int, val y: Int

fun main(){
    val point = Point(1, 1)
    println(point)
    println(point.copy(x = 10))
    println(point.copy(y = 10))
}

And the output is:

Point(x=1, y=1)
Point(x=10, y=1)
Point(x=1, y=10)

It’s a very convenient feature which helps with immutability on the domain classes.

Another difference is that Kotlin’s data classes allow the properties to be mutable. Let’s take a look at the declaration again:

data class Point(val x: Int, val y: Int)

It declares each property as val which in Java terms is final which means that the following code won’t compile:

val point = Point(1, 1)
point.x = 10;

However, if declared using var (as in variable):

data class Point (var x: Int, var y: Int)

then the code below compiles:

val point = Point(1, 1)
point.x = 10

Mutable vs Immutable objects was a discussion where immutability won and the fact that the properties in a Java Record are all immutable by default is something I cherish. However I miss the convenience of the copy method. Hopefully it will appear in the future releases.

Just for completeness this is the Kotlin equivalent of the example covered in the Java Records article:

data class Rotator(val pitch: Int, val roll: Int, val yaw: Int)
data class Vector(val x: Int, val y: Int, val z: Int)
data class Transform(val rotation: Rotator, val translation: Vector, val scale: Vector) 

fun main() {
    val scale = Vector(1, 1, 1)
    val translation = Vector(100, 0, 0)
    val rotation = Rotator(0, 0, 90)
    val t1 = Transform(rotation, translation, scale) println(t1)
    println(t1.translation.x)
    println(t1.rotation.yaw)
}

Java Records

Java 14 introduces a new language feature, a Record.

Records provide a compact syntax for declaring classes which are transparent holders for shallowly immutable data.

The semantic goal of Records is to enable modeling data as data. Records are clear and concise declarations of shallowly-immutable, well-behaved nominal data aggregates.

A Record has a name and a state description. The state description declares the components of the record.
The classic example is a 2D point:

record Point(int x, int y) {}

A Record is a new kind of type declaration. It’s a restricted form of class similar to an enum. The API of a Record is defined by its representation unlike classes which have the ability to decouple API from representation. Therefore, a Record acquires the following members automatically:

  • A private final field for each component of the state description
  • A public read accessor method for each component of the state description
  • A public constructor, whose signature is the same as the state description
  • Implementations of equals() and hashCode()
  • An implementation of toString()

However, a Record has the following restrictions:

  • Cannot extend any class
  • Cannot be extended
  • Cannot have non-final instance fields

Records support all other features of normal classes. They can implement interfaces, have instance methods, constructors, all kinds of static members, annotations and everything else.

A more sophisticated example of how Records can help with modeling data would be the idea of describing a 3D object with a transform which has information about rotation, translation and scale.

record Rotator(int pitch, int roll, int yaw) {}
record Vector(int x, int y, int z) {}
record Transform(Rotator rotation, Vector translation, Vector scale) {}

No other code is needed to start using our transform data model:

Vector scale = new Vector(1, 1, 1);
Vector translation = new Vector(100, 0, 0);
Rotator rotation = new Rotator(0, 0, 90);
Transform t1 = new Transform(rotation, translation, scale);

System.out.println(t1);
System.out.println(t1.translation().x());
System.out.println(t1.rotation().yaw());

The example above produces the following output:

Transform[rotation=Rotator[pitch=0, roll=0, yaw=90], translation=Vector[x=100, y=0, z=0], scale=Vector[x=1, y=1, z=1]]
100
90

How much code would be needed to achieve the same with normal classes is left to the imagination of the reader.

Of course, Records can be used for much more. Some of the obvious use-cases include:

  • Multiple return values or tuples
  • Data transfer objects
  • Compound map keys
  • Messages
  • Value wrappers