Java 14 - Records Preview Feature (JEP 359)

In Java an object is created from a class. Java also adopt Object Oriented Programming (OOP) concept like encapsulation, and setter and getter methods are used for updating and retrieving the value of a variable.

Traditional Java Class

The following code is an example of a simple class with a private variable and a couple of getter/setter methods:

import java.time.LocalDate;

public class Product {

    private long id;
    private String code;
    private String name;
    private LocalDate releaseDate;
    
    /**
     * @return the id
     */
    public long getId() {
        return id;
    }

    /**
     * @param id the id to set
     */
    public void setId(long id) {
        this.id = id;
    }

    /**
     * @return the code
     */
    public String getCode() {
        return code;
    }

    /**
     * @param code the code to set
     */
    public void setCode(String code) {
        this.code = code;
    }

    /**
     * @return the name
     */
    public String getName() {
        return name;
    }

    /**
     * @param name the name to set
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * @return the releaseDate
     */
    public LocalDate getReleaseDate() {
        return releaseDate;
    }

    /**
     * @param releaseDate the releaseDate to set
     */
    public void setReleaseDate(LocalDate releaseDate) {
        this.releaseDate = releaseDate;
    }

    @Override
    public String toString() {
        return "Product{" + "id=" + id + ", code=" + code + ", name=" + name + ", releaseDate=" + releaseDate + '}';
    }
}
                    

Now we start to have problem. Above example is only a simple class. With several lines added, we only achieve to protect the private variables with setters and getters. But what the class must have more than that? How about constructors, equals() and hashCode(), toString() method? And many more, just for a simple class. Many programmers sharing the same (painful) experience; need to add low-value and repetitive codes only to make their classes have "the basic usability". The tools and libraries also exists out there, to help improve developer's experience.

I remember in the beginning I need to code myself the setter and getter. Then the IDE come with the tool to generate it. Similarly for constructors or any "basic" methods that I mention above. Then, in 2009 we have Project Lombok, spicing up our Java codes. Above class now can be code as simple as:

import java.time.LocalDate;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@ToString
public class Product {

    private long id;
    private String code;
    private String name;
    private LocalDate releaseDate;
}
                    

With lombok, we just need annotate any field with @Getter and/or @Setter. The annotation also working in class level, lombok then will generate the default getter/setter automatically.

But the "progress" continue, to reduce these boilerplate codes...

Introducting: Records

Java 14 tries to solve this issue by introducing a new type called record, with JEP 359. The same class from the example above could be written as a record, like this:

package com.dariawan.jdk14.records;

import java.time.LocalDate;

public record Product (
    long id,
    String code,
    String name,
    LocalDate releaseDate) {
}
                    

To compile, since record is a preview feature, you need to specify option --release 14--enable-preview

$ javac --release 14 --enable-preview com\dariawan\jdk14\records\Product.java
Note: com\dariawan\jdk14\records\Product.java uses preview language features.
Note: Recompile with -Xlint:preview for details.

Let's disassemble the Product.class file with javap command with option -p to shows all members:

$ javap -p com.dariawan.jdk14.records.Product
Warning: File .\com\dariawan\jdk14\records\Product.class does not contain class com.dariawan.jdk14.records.Product
Compiled from "Product.java"
public final class com.dariawan.jdk14.dto.Product extends java.lang.Record {
  private final long id;
  private final java.lang.String code;
  private final java.lang.String name;
  private final java.time.LocalDate releaseDate;
  public com.dariawan.jdk14.dto.Product(long, java.lang.String, java.lang.String, java.time.LocalDate);
  public java.lang.String toString();
  public final int hashCode();
  public final boolean equals(java.lang.Object);
  public long id();
  public java.lang.String code();
  public java.lang.String name();
  public java.time.LocalDate releaseDate();
}

From above de-compiled class, here what we have:

  • A final class, class is non extendable
  • private final field for all four fields
  • A public constructor having all fields.
  • Implementation of toString().
  • Implementation of equals() and hashCode().
  • Getter for each fields with the same name and type (no setter) — we will explore more about this on below.

As we can see, no available available setter for the field. All the assignments must be done from constructor.

Product prod = new Product(888L, "PRD888",
        "Amazing Vocal Microphone",
        LocalDate.of(2020, Month.MARCH, 25));
// prod.setName("Amazing Subwoofer")  // will not work
System.out.println(prod);
                    

Records Restriction and Limitation:

  • Records cannot extend any class, although they can implement interfaces
  • Records cannot be abstract
  • Records are implicitly final, you cannot inherit from a record
  • Records can have additional fields in the body, but only if they are static
  • Records are immutable as all state components are final.

Adding methods

As the name specify, record purpose is to holding data without any functionality. But, we still able to add our own custom methods. Since records are immutable, we cannot change any state, or we will get this error:

cannot assign a value to final variable name

Here the full product record:

Product.java
package com.dariawan.jdk14.records;

import java.time.LocalDate;

public record Product (
    long id,
    String code,
    String name,
    LocalDate releaseDate) {

    public boolean isFutureRelease() {
        return releaseDate.isAfter(LocalDate.now());
    }
}
                    

And class JEP359ProductExample to work with Product:

JEP359ProductExample.java
package com.dariawan.jdk14;

import com.dariawan.jdk14.records.Product;
import java.time.LocalDate;
import java.time.Month;

public class JEP359ProductExample {
 
    public static void main(String[] args) {
        Product prod = new Product(888L, "PRD888",
                "Amazing Vocal Microphone",
                LocalDate.of(2020, Month.MARCH, 25));
        // prod.setName("Amazing Subwoofer")  // will not work
        System.out.println(prod);
        System.out.println("Future release: " + prod.isFutureRelease());
        
        prod = new Product(999L, "PRD99",
                "Amazing Synthesizer",
                LocalDate.of(2027, Month.MAY, 7));
        System.out.println(prod);
        System.out.println("Future release: " + prod.isFutureRelease());
    }
}
                    

And when running it, here the result:

Product[id=888, code=PRD888, name=Amazing Vocal Microphone, releaseDate=2020-03-25] Future release: false Product[id=999, code=PRD99, name=Amazing Synthesizer, releaseDate=2027-05-07] Future release: true

Record and Reflection

Our Product record contains getter methods for all four fields, and we have no setter (and remember above error: cannot assign a value to final variable name).

System.out.println("Id : " + prod.id()); System.out.println("Code : " + prod.code()); System.out.println("Name : " + prod.name()); System.out.println("ReleaseDate: " + prod.releaseDate());

But somehow, we still assign values using reflection, like this:

Field fld = null; try { fld = prod.getClass().getDeclaredField("code"); fld.setAccessible(true); fld.set(prod, "PRO111"); System.out.println("New code: " + prod.code()); } catch (Exception e) { e.printStackTrace(); }

You can check if a class is a record using method isRecord(). It'll return true if the class is a record class. Use method getRecordComponents() to return all record components of a record class. It'll return null if the class is not a record class:

if (Product.class.isRecord()) { System.out.println("Product is a record, and record's components are:"); RecordComponent[] cs = Product.class.getRecordComponents(); for (RecordComponent c : cs) { System.out.println(" >> " + c); } }

Here the complete sample codes:

JEP359RecordReflection.java
package com.dariawan.jdk14;

import com.dariawan.jdk14.records.Product;
import java.lang.reflect.Field;
import java.lang.reflect.RecordComponent;
import java.time.LocalDate;
import java.time.Month;

public class JEP359RecordReflection {
 
    public static void main(String[] args) {
        Product prod = new Product(111L, "PRD111",
                "New Multiplayer Game",
                LocalDate.of(2020, Month.MARCH, 31));
        System.out.println(prod);
        System.out.println("Id         : " + prod.id());
        System.out.println("Code       : " + prod.code());
        System.out.println("Name       : " + prod.name());
        System.out.println("ReleaseDate: " + prod.releaseDate());
        
        Field fld = null;
        try {
            fld = prod.getClass().getDeclaredField("code");
            fld.setAccessible(true);
            fld.set(prod, "PRO111");
            System.out.println("New code: " + prod.code());
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        if (Product.class.isRecord()) {
            System.out.println("Product is a record, and record's components are:");
            RecordComponent[] cs = Product.class.getRecordComponents();
            for (RecordComponent c : cs) {
                System.out.println(" >> " + c);
            }
        }
    }
}
                    

Here the result when we run it:

Product[id=111, code=PRD111, name=New Multiplayer Game, releaseDate=2020-03-31] Id : 111 Code : PRD111 Name : New Multiplayer Game ReleaseDate: 2020-03-31 New code: PRO111 Product is a record, and record's components are: >> long id >> java.lang.String code >> java.lang.String name >> java.time.LocalDate releaseDate