DEV Community

Cover image for Records in JDK16 (JEP 395)
Ashish Garg
Ashish Garg

Posted on • Updated on

Records in JDK16 (JEP 395)

Overview

JEP 359 (Records) added as preview feature in JDK 14 and stable production ready feature with JEP 395 in
JDK 16. Records are part of larger release named as project
Amber (Umbrella project having multiple feature
targeted in various releases).

Records are primarily designed to target the use-case where classes are getting created, just to
hold data (DTOs). Usually in beginning developer create a data aggregate class with the pure
intention to hold data but during course of project it got corrupted with various business logic
(functional methods) and lost its flavour of pure data aggregate and become a nightmare to manage.
You might be thinking of using Record in place of JPA Entities, why it's not possible I will
explain later.

Let’s say in today’s world developer like to create a data aggregate class, he/she will create
something like below with required fields. I have created a Fund class with some fields like
nameOfFund, IdOfFund etc.

public class Fund {

    private String nameOfFund;
    private String IdOfFund;
    private FundType typeOfFund;
    private float unitOfPrice;
    private String nameOfFundManager;
    private float exitLoad;
    private float aum;
    private boolean lockIn;
}

enum FundType{
    LARGE_CAP,
    MID_CAP,
    SMALL_CAP,
    MULTI_CAP,
    HYBRID
}
Enter fullscreen mode Exit fullscreen mode

As you might have noticed, this is a simple class with different fields representing state of
class but for a developer who will read it later there is no explicit communication from language
semantics that this is a data aggregate not a Java Bean, also it does not have certain required
methods i.e. equals(), hashCode(), toString(), Constructors and Getters to make it useful.

Writing all these methods introduce a lot of boilerplate code for a developer to write, even most
of the IDEs in today’s world can generate these methods still it’s a brain exercise for developer
who has to read it and there is always a possibility at some day developer will add/remove
certain fields from it but did he rightly update all relevant method (equals & hashCode) will
always be question?

Let’s see how a Fund class will look like after all required methods generated by our
favourite IDE (I have used IntelliJ Community Edition to generate these methods)

public class Fund {

    private String nameOfFund;
    private String IdOfFund;
    private FundType typeOfFund;
    private float unitOfPrice;
    private String nameOfFundManager;
    private float exitLoad;
    private float aum;
    private boolean lockIn;

    public Fund(String nameOfFund, String idOfFund, FundType typeOfFund, float unitOfPrice, String nameOfFundManager, float exitLoad, float aum, boolean lockIn) {
        this.nameOfFund = nameOfFund;
        IdOfFund = idOfFund;
        this.typeOfFund = typeOfFund;
        this.unitOfPrice = unitOfPrice;
        this.nameOfFundManager = nameOfFundManager;
        this.exitLoad = exitLoad;
        this.aum = aum;
        this.lockIn = lockIn;
    }

    public String getNameOfFund() {
        return nameOfFund;
    }

    public void setNameOfFund(String nameOfFund) {
        this.nameOfFund = nameOfFund;
    }

    public String getIdOfFund() {
        return IdOfFund;
    }

    public void setIdOfFund(String idOfFund) {
        IdOfFund = idOfFund;
    }

    public FundType getTypeOfFund() {
        return typeOfFund;
    }

    public void setTypeOfFund(FundType typeOfFund) {
        this.typeOfFund = typeOfFund;
    }

    public float getUnitOfPrice() {
        return unitOfPrice;
    }

    public void setUnitOfPrice(float unitOfPrice) {
        this.unitOfPrice = unitOfPrice;
    }

    public String getNameOfFundManager() {
        return nameOfFundManager;
    }

    public void setNameOfFundManager(String nameOfFundManager) {
        this.nameOfFundManager = nameOfFundManager;
    }

    public float getExitLoad() {
        return exitLoad;
    }

    public void setExitLoad(float exitLoad) {
        this.exitLoad = exitLoad;
    }

    public float getAum() {
        return aum;
    }

    public void setAum(float aum) {
        this.aum = aum;
    }

    public boolean isLockIn() {
        return lockIn;
    }

    public void setLockIn(boolean lockIn) {
        this.lockIn = lockIn;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Fund)) return false;
        Fund fund = (Fund) o;
        return Float.compare(fund.unitOfPrice, unitOfPrice) == 0 &&
                Float.compare(fund.exitLoad, exitLoad) == 0 &&
                Float.compare(fund.aum, aum) == 0 &&
                lockIn == fund.lockIn &&
                nameOfFund.equals(fund.nameOfFund) &&
                IdOfFund.equals(fund.IdOfFund) &&
                typeOfFund == fund.typeOfFund &&
                nameOfFundManager.equals(fund.nameOfFundManager);
    }

    @Override
    public int hashCode() {
        return Objects.hash(nameOfFund, IdOfFund, typeOfFund, unitOfPrice, nameOfFundManager, exitLoad, aum, lockIn);
    }

    @Override
    public String toString() {
        return "Fund{" +
                "nameOfFund='" + nameOfFund + '\'' +
                ", IdOfFund='" + IdOfFund + '\'' +
                ", typeOfFund=" + typeOfFund +
                ", unitOfPrice=" + unitOfPrice +
                ", nameOfFundManager='" + nameOfFundManager + '\'' +
                ", exitLoad=" + exitLoad +
                ", aum=" + aum +
                ", lockIn=" + lockIn +
                '}';
    }
}

enum FundType{
    LARGE_CAP,
    MID_CAP,
    SMALL_CAP,
    MULTI_CAP,
    HYBRID
}
Enter fullscreen mode Exit fullscreen mode

Record

To address such concerns JEP 359 comes with a new type declaration record like enum we
already have. Records as name specify is purely mean for having/holding data without any
functionality. It also helps to reduce a lot of boilerplate code which developer need to write
to create a data aggregate. In today’s world there is no mean to communicate that developer
created a class with the pure intention of data aggregate, record will make this communication very
crystal clear.

record Fund(String nameOfFund, String idOfFund, FundType typeOfFund, float unitPrice, String nameOfFundManager,
        float exitLoad, float aum, boolean lockIn) {}
Enter fullscreen mode Exit fullscreen mode

In above code snippet record type has been specified just before name and required fields
has been placed with their type under braces () that’s all you need to do as a developer to create
data aggregate. Record comply with the property of immutable class.

As mentioned in effective java also “Classes should be immutable unless there’s a very good
reason to make them mutable.If a class cannot be made immutable, limit its mutability as much
as possible
” Generally to make an immutable class developer need to make sure:

  • Class must not be extendable — make the class final, or use static factories and keep constructors private
  • Make fields private final
  • Do not any declare any method which can change state of object after creation

As definition a record class declaration consists of a name, optional type parameters, a header,
and a body. The header lists the components of the record class, which are the variables that
make up its state. In case of record Compiler will create an Immutable class based on record header declaration
with below features:

  • For each component in header there will be a private final field and Getter for each component with the same name and type as specified in state
  • A canonical constructor with the same name as record and having all fields with type
  • Implementation of equals() and hashCode()
  • Implementation of toString()

If you want to see what all methods have been created by compiler simply run javap on *.class file (Compilation instructions has been provided at the end of this article)

In above snapshot your can see record class is final and extending java.lang.Record

Rules for Constructor

  • A normal class without any explicit constructor declaration got default constructor from java compiler that's not true for record; record class without any constructor declarations is automatically given a canonical constructor that assigns all the private fields to the corresponding arguments.
  • Canonical constructor may be declared explicitly with a list of formal parameters which match the record header.
  • There is another form of constructor defined in Record named as Compact canonical constructor the parameters are declared implicitly, and the private fields corresponding to record components cannot be assigned in the body but are automatically assigned to the corresponding formal parameter (this.x = x;) at the end of the constructor. The compact form helps developers focus on validating and normalizing parameters without the tedious work of assigning parameters to fields.
  • A record class can be declared top level or nested, and can be generic.
  • A record class can declare static methods, fields, and initializers.
  • A record class can declare instance methods.
  • A record class can implement interfaces and declare instance methods to implement them.
  • A record class can declare nested types, including nested record classes. If a record class is itself nested, then it is implicitly static; this avoids an immediately enclosing instance which would silently add state to the record class.
  • Instances of record classes can be serialized and deserialized. However, the process cannot be customized by providing writeObject, readObject, readObjectNoData, writeExternal, or readExternal methods. The components of a record class govern serialization, while the canonical constructor of a record class governs deserialization.

Restriction on Records

  • Record class cannot extend any other class. The superclass of your record class will always be java.lang.Record, even though a normal class can explicitly extend and there implicit superclass is java.lang.Object.
  • Records cannot declare instance field and cannot contain instance initializers.
  • Records are implicitly final and cannot be abstract
  • Records are immutable as all state components are final.
  • Any explicit declaration of member must match the type of the automatically derived member.
  • Any explicit implementation of accessors or the equals or hashCode methods should be careful to preserve the semantic invariants of the record class
  • A record class cannot declare native methods. If a record class could declare a native method then the behavior of the record class would by definition depend on external state rather than the record class's explicit state.

Local Record classes

  • Local records are implicitly static this is in contrast of local classes which are not implicitly static.

Reflection

Two new methods has been added in reflection API for records.

  • getRecordComponents() — Returns an array of **RecordComonent** (This is a new class added in JDK representing a record component) objects representing all the record components of this record class, or **null** if this class is not a record class. Components will be return in the same order as declared while creating record. After getting array of RecordComponent you can iterate individual element and access name, type etc.
  • isRecords() — Returns true if and only if this class is a record class.

As applicable with developer created Immutable class you can change value of a particular filed via reflection.

Field fld = null;
try {
    fld = fund.getClass().getDeclaredField("aum");
    fld.setAccessible(true);
    fld.setFloat(fund,120f);
    System.out.println(fund.aum);
} catch (NoSuchFieldException | IllegalAccessException e) {
    e.printStackTrace();
}
Enter fullscreen mode Exit fullscreen mode

As shown in above sample, you just need to call setAccessible(true) to update record state. In my opinion as Java added a new type, it shouldn’t allow record state to be change via reflection but that’s my personal opinion JDK designer must be some better reason to allow it.

Serialization

Record support Serialization but there are certain changes has been done in JDK.

  • Record components/fields are deserialized prior to the invocation of record constructor.
  • For normal class while serialize/deserialize we can override certain methods (writeObject, readObject etc.) to specify custom behaviour. Its not allowed in record.
  • The serialVersionUID of a record class is 0L unless explicitly declared
  • While deserialization system did not match value of serialVersionUID.

Record with Sealed classes

Record work pretty well with upcoming feature Sealed classes (It's under preview right now).

    sealed interface MutualFund permits LargeCap,MidCap,SmallCap {}
        record LargeCap(double AUM) implements MutualFund{}
        record MidCap(double AUM) implements MutualFund{}
        record SmallCap(double AUM) implements MutualFund{}
Enter fullscreen mode Exit fullscreen mode

Important Points

  • As compiler will create getter method with the same name as field, developer need to conscious enough to provide a good context specific readable name.
  • Record body may declare static methods, static fields, static initializers, constructors, instance methods, and nested types.
  • There is very important difference between a class and record.

A class can have field ‘name’, constructor can have an argument ‘name’ and a getter method can have name; all three exist without any relationship but in case of record all three will be referring to same component/field. Here is sample class where there is no relationship between instance field ‘name’, constructor argument ‘name’ and method ‘getname()’.

public class Sample {

    private static String name = "Fun";

    Sample(){
    }

    Sample(String name){
        System.out.println(name);
    }

    private String getname(){
        return "Ashish";
    }

    public static void main(String[] args) {
        System.out.println(name);
        System.out.println(new Sample().getname());
        new Sample("Dummy");
    }
}
Enter fullscreen mode Exit fullscreen mode

if you run above program you will see a output where three different values are getting print on console.

  • You can’t create a subclass of java.lang.Record, any such attempt will throw error **“records cannot directly extend Record”**.
  • As mentioned above compiler will create a canonical constructor for you based on the definition provided in record header but there may be case where you like to put kind of validation it has been allowed
public Fund{
    if (nameOfFund==null || idOfFund==null){  
        throw new IllegalArgumentException("Custom exception");
    }
}
Enter fullscreen mode Exit fullscreen mode
  • Record should be used where you are missing struct in Java.
  • Record will remove the need of libraries like Lombok.
  • Record and Inline classes (part of project Valhalla) are two very different things.

Top comments (0)