Java 14 new features: Records
This is the second article in the blog post series discussing the new features introduced in java 14. Today's article is focused on Records
that aims to provide a compact & concise way for declaring data classes.
Java 14 rew features articles:
- Pattern Matching for
instanceof
,jpackage
& helpful NPEs - Switch Expressions, JFR Event Streaming and more
- Text Blocks & Foreign-Memory Access API
Why ?
Java is verbose!
You've for sure already heard this statement before, from your colleagues, at a conference, probably in meetups, or you already saw it in twitter or reddit!
Brian Goetz](https://twitter.com/BrianGoetz), Java Language Architect at Oracle, wrote a detailled post in that matter, stating that, as example, developers who want to create simple data carrier classes in a way that are easy to understand have to write a lot of low-value, repetitive, error-prone code: constructors
, accessors
, equals()
, hashCode(), toString()
...
To avoid the frustration, some make use of IDE capabilities to do the legwork of writing the boilerplate, but fail to consider much beyond functionality of the code itself to help the reader distill the design intent. Others use some libraries such as Lombok, While the lazy ones just omit the those methods, leading to surprising behavior and poor debuggability.
A new type declaration: Record!
Records are a special kind of lightweight classes in java, intended to be simple data carriers, similar to what exist in other languages (such as case
classes in Scala, data
classes in Kotlin and record
classes in C#). The aim is to extend the Java language syntax and create a way to say that the type represents only data. By making this statement, We're telling the compiler to do all the work for us and produce the methods without any effort from outside.
Show me the code
Let start with the following Person
record:
public record Person(
String firstName,
String lastName,
int age,
String address,
Date birthday
){}
The record class is an immutable, transparent carrier for a fixed set of fields known as the record components
that provides a state
description for the record. Each component gives rise to a final
field that holds the provided value and an accessor
method to retrieve the value. The field name and the accessor name match the name of the component.
Let's now try to compile the Person
class. Since records still a preview language feature
, which means that we need to enable the preview flag:
javac --enable-preview -source 14 Person.java
Now if we examen the class file with javap
, you can see that the compiler has autogenerated a bunch of boilerplate code:
$ javap Person Compiled from "Person.java"
public final class Person extends java.lang.Record {
public Person(java.lang.String, java.lang.String, int, java.lang.String, java.util.Date);
public java.lang.String toString();
public final int hashCode();
public final boolean equals(java.lang.Object);
public java.lang.String firstName();
public java.lang.String lastName();
public int age();
public java.lang.String address();
public java.util.Date birthday();
}
Notice a couple of things here:
- a private
final
field, with the same name and type, for each component in the state description; - a public read
accessor
method, with the same name and type, for each component in the state description; - a public
constructor
, whose signature is the same as the state description, which initializes each field from the corresponding argument; - implementations of
equals
andhashCode
that say two records are equal if they of the same type and contain the same state; - implementation of
toString
that includes all the components, with their names.
Looking further and examining the byte code, we notice that both hashCode
, equals
and toString
rely on invokedynamic
to dynamically invoke the appropriate method containing the implicit implementation.
public java.lang.String toString();
descriptor: ()Ljava/lang/String;
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokedynamic #32, 0 // InvokeDynamic #0:toString:(LPerson;)Ljava/lang/String;
6: areturn
LineNumberTable:
line 2: 0
public final int hashCode();
descriptor: ()I
flags: (0x0011) ACC_PUBLIC, ACC_FINAL
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokedynamic #36, 0 // InvokeDynamic #0:hashCode:(LPerson;)I
6: ireturn
LineNumberTable:
line 2: 0
public final boolean equals(java.lang.Object);
descriptor: (Ljava/lang/Object;)Z
flags: (0x0011) ACC_PUBLIC, ACC_FINAL
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: invokedynamic #40, 0 // InvokeDynamic #0:equals:(LPerson;Ljava/lang/Object;)Z
7: ireturn
LineNumberTable:
line 2: 0
Can I define additional methods, fields...
The short answer to this question is Yes, you can add static fields/methods! But the question is however, should you ?!
Keep in mind that the goal behind Records is to enable developers to group related fields together as a single immutable data item without the need to write verbose code. Which means that whenever you feel the temptation to add more fields/methods to your record
, think if a full class makes more sens and should be used instead.
For example, we can define a method that returns a Person
's full name:
public record Person(
String firstName,
String lastName,
int age,
String address,
Date birthday
){
public String fullName(){
return firstName + " " + lastName;
}
}
Compact constructor
Additionally, Records introduced Compact Constructor
, with the aim that only validation and/or normalization code need to be given in the constructor body. The remaining initialization code is supplied by the compiler.
For example, if we want to validate a Person
age to make sure that it's not negative, the code would looks similar to:
public record Person(
String firstName,
String lastName,
int age,
String address,
Date birthday
){
public Person{
if (age < 0) {
throw new IllegalArgumentException( "Age must be greater than 0!");
}
}
}
Notice that no explicit parameter list is given for the compact constructor, but is derived from the record component list.
Final word
Records address a common issue with using classes as wrappers for data. Plain data classes are significantly reduced from several lines of code to a one-liner.
Keep in mind that Records are a preview language feature, which means that, although it is fully implemented, it is not yet standardized in the JDK.