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:

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 and hashCode 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.


Ressources: