days
0
-18
0
hours
-1
-8
minutes
-2
0
seconds
-4
-3
search
Celebrating Java 14

Hands on with Records in Java 14 – A Deep Dive

Mala Gupta
Java
© Shutterstock / Kirasolly

To celebrate the release of Java 14, here’s a deep dive into Records in JDK 14. It’s written by Developer Advocate at JetBrains, founder of eJavaGuru.com and Java Champion, Mala Gupta. What a treat! So let’s get stuck in.

With Records, Java 14 is welcoming compact syntax for declaring data classes. It lets you model your data with ease – by using just a single line of code. The compiler would do the heavy lifting in terms of the implicit methods that are required to make the class work with you.

In this article, I’d cover what records are, the issues they address, and how to use them. Let’s get started.

Why do we need Records in Java?

It is rare to see an application that doesn’t interact with a data store. Often your application would need to read data from a data source or write to it. As developers, we are used to defining a data class, encapsulating variables to store its state. However, with a regular class, the rules are to define private instance variables to store the state of your data object, add public constructors, accessor or mutators methods and override methods like toString(), equals() and hashCode() from the Object class.

At times, developers cut down on their efforts – either by not defining these methods, or by using an existing class that is vaguely like the one they require. Both the cases may throw up surprises later.

Following is an example of a class you could use to represent a Rectangle and store it to a datastore:

package com.jetbrains;
import java.util.Objects;

public class Rectangle {
    private int length;
    private int breadth;

    public Rectangle(int length, int breadth) {
        this.length = length;
        this.breadth = breadth;
    }

    public int getLength() {
        return length;
    }

    public void setLength(int length) {
        this.length = length;
    }

    public int getBreadth() {
        return breadth;
    }

    public void setBreadth(int breadth) {
        this.breadth = breadth;
    }

    @Override
    public String toString() {
        return "Rectangle{" + "length=" + length + ", breadth=" + breadth + '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Rectangle = (Rectangle) o;
        return length == rectangle.length && breadth == rectangle.breadth;
    }

    @Override
    public int hashCode() {
        return Objects.hash(length, breadth);
    }
}

Though we could use IDEs like IntelliJ IDEA to generate all this code (constructors, getters, setters and override methods from Object), a class couldn’t be tagged just as a data class. Without any help from the language, a developer would still need to read through all the code to realize that it is just a data class. Let’s see how records can help.

What is a Record?

Records introduce a new type declaration in Java, which simplifies the task of modelling your data as data. It also cuts down significantly on the boilerplate code (however, this isn’t the primary reason for its introduction).

Let’s define the class Rectangle (from preceding section) as a Record:

record Rectangle(int length, int breadth) {}

With just one line of code, the preceding example defines a record with components length and breadth. By default, the compiler creates a bunch of methods for a record, like instance methods to store its state, constructors, getters (no setters-the intent to the data be immutable), and overrides toString(), equals() and hashCode() method from the Object class.

In IntelliJ IDEA 2020.1, ‘New Java Class’ dialog lets you choose your type as ‘Record (Preview Feature)’:

java

It is interesting to know what happens when you compile your records and the implicit members that are added to it by the compilation process.

Implicit members added to a record

Defined as a final class, a record implicitly extends the java.lang.Record class from the core Java API. For each of the components (data variables) it defines, the compiler defines a final instance variable. Interestingly, the name of the getter methods is same as the name of the data variable (and don’t start with ‘get’). Since a record is supposed to be immutable, no setter methods are defined.

The methods toString(), hashCode() and equals() are also generated automatically for records.

You can use the Java Class File Disassembler (javap.exe from JDK with option -p) to show the variables and methods of a decompiled class.

You can also open a .class file in IntelliJ IDEA to view its decompiled version. Just click on its .class file in the ‘out’ folder:

java

SEE ALSO: Pattern Matching for instanceof in Java 14

How to use a record

You can initialize a record like any other class by using the operator new, passing values to its constructor, defining its state. Let’s instantiate the record Rectangle defined in the previous section:

Rectangle smallRectangle = new Rectangle(2, 5);
Rectangle aBigRectangle = new Rectangle(100, 150);

Let’s call a couple of (implicit) methods on these references, which were added to it by the compilation process:

System.out.println("Let's see what toString() returns: " + smallRectangle);
System.out.println(smallRectangle.equals(aBigRectangle));
System.out.println("Let's getTheHashCode() :" + smallRectangle.hashCode());
System.out.println("Seriously? no getLength() method: " + smallRectangle.length());

Here’s the output of the preceding code:

Let's see what toString() returns: Rectangle[length=2, breadth=5]
false
Let's getTheHashCode() :67
Seriously? no getLength() method: 2

As evident in the preceding output, the toString() method includes the name of the record, followed by the name of its state variables and their values. Another noticeable point is that the getters for record properties don’t include the word ‘get’; they are the same as the property name. Also, no setter methods (since records are supposed to be immutable).

Support of Java 14 features in IntelliJ IDEA

The latest version of IntelliJ IDEA – 2020.1 fully supports all the new language features of Java 14. You can download the Beta version now and check it out yourself.

Configure IntelliJ IDEA 2020.1 to use JDK 14 for Project SDK and choose the Project language level as ‘14 (Preview) – Records, patterns, text blocks’ for your Project and Modules settings.

java

The Ultimate version is free to use in its Early Access Program (EAP).

Modifying the default behavior of a Record

You can modify the default implementation of the methods that are generated by a compiler. However, you must not do that unless it is required.

The default implementation of the constructor generated by a compiler, initializes the state of a record. You can modify this default implementation to validate the argument values or to add business logic.

IntelliJ IDEA lets you insert a Compact, Canonical or Custom constructor, when you invoke action ‘Generate’, by using Alt + Insert on Windows or Linux system/ or ^ N on macOS:

A compact constructor doesn’t define a parameter list, not even parenthesis. Here’s an example:

package com.jetbrains;
public record Rectangle {     // no parameter list here!
    public Rectangle {
        if (length < 0 || breadth < 0) throw new IllegalArgumentException("Length & breadth must be > 0");
    }
}

Here’s an example of a canonical constructor to validate the parameter values passed to its constructor. This constructor also adds in some business logic – incrementing a static field in the constructor:

public record Rectangle(int length, int breadth) {
    public Rectangle {
        if (length < 0 || breadth < 0) {
            throw new IllegalArgumentException("-ve dimensions. Seriously?");
        }
        totalRectanglesCreated++;
    }
 
    private static int totalRectanglesCreated;
    static int getTotalRectanglesCreated() {
        return totalRectanglesCreated;
    }
}

As you can notice in the preceding example, a record can add static fields or instance and static methods, if required.

By inserting a custom constructor in IntelliJ IDEA, you can choose the parameters you want to explicitly accept in a constructor. The remaining state variable would be assigned a default value (for example null for reference variables, 0 for integer values).

You can’t add multiple constructors to a record like a regular class.

SEE ALSO : Java 14 – “Small changes can significantly improve the developer experience”

Defining a Generic record

You can define record with generics. Here’s an example of a record, say, Parcel, which can store any object as its contents, and capture the parcel’s dimensions and weight:

public record Parcel<T>(T contents, 
    double length, 
    double breadth, 
    double height, 
    double weight) {}
// You can instantiate this record as follows :
class Table{ /* class code */ } 
public class Java14 {
    public static void main(String[] args) {
        Parcel
<Table> parcel = new Parcel<>(new Table(), 200, 100, 55, 136.88);
        System.out.println(parcel);
    }
}

Adding annotations to record components

You can add annotation to the components of a record, which are applicable to them. For example, you could define a record Car, applying @NotNull annotation to its component model:

package com.jetbrains;
import org.jetbrains.annotations.NotNull;
import java.time.LocalDateTime;

public record Car(@NotNull String model, LocalDateTime purchased){}

If you pass a null value as the first parameter while instantiating record Car, you’ll get a warning.

SEE ALSO: Java 14 – “Regression and compatibility tests are essential”

Reading and Writing Records

Writing a record to a file and reading it using the Java File I/O API is simple. Let your record implement the Serializable interface and you are good to go. Here’s the modified version of the record Rectangle, which you can write to and read from a file:

package com.jetbrains;
public record Rectangle(int length, int breadth) implements Serializable {}
public class Java14 {
    public static void main(String[] args) {
        Rectangle smallRectangle = new Rectangle(2, 5);
        writeToFile(smallRectangle, "Java14-records");
        System.out.println(readFromFile("Java14-records"));
    }
    static void writeToFile(Rectangle obj, String path) {
        try (ObjectOutputStream oos = new ObjectOutputStream( new FileOutputStream(path))){
            oos.writeObject(obj);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    static Rectangle readFromFile(String path) {
        Rectangle result = null;
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(path))){
            result = (Rectangle) ois.readObject();
        } catch (ClassNotFoundException | IOException e) {
            e.printStackTrace();
        }
        return result;
    }
} 

There are always two sides to a coin. With a lot of advantages to records, let’s see what is it that you can’t do with it.

Are there any restrictions on a record?

Since a record is a final class, you can’t define it as an abstract class. Also, it can’t extend another class (since it implicitly extends java.lang.Record class). But there are no restrictions on it implementing interfaces.

Even though you can add static members to a record, you can’t add instance variables to it. This is because a record is supposed to limit the instance members to the components it defines in the record definition. Also, you can override the default implementation of the methods which are generated by a compiler for a record – like its constructor, methods equals(), hashCode(), and toString.

A restricted identifier

record’ is a restricted identifier (like ‘var’) and isn’t a keyword (yet). So, the following code is valid:

int record = 10;
void record() {}

However, you should refrain from using record as an identifier because it could be included as a keyword in a future Java version.

Preview Language Feature

Records have been released as a preview language feature in Java 14. With Java’s new release cadence of six-month, new language features are released as preview features, which are neither incomplete nor half-baked features. A preview language feature essentially means that even though this feature is ready to be used by developers, its finer details could change in a future Java release – depending on the feedback received on this feature by developers. Unlike an API, language features can’t be deprecated in future. So, if you have any feedback on text blocks, share it on the JDK mailing list (membership required).

If you are using command prompt, you must enable them during compilation and runtime. To compile class Rectangle, which defines a record, use the following command:

javac --enable-preview --release 14 Java14.java

The compiler would warn that you are using a preview language feature.

To execute a class, say, Java14, that uses records (or another preview language feature in Java 14), use the following command:

java --enable-preview Java14

Looking ahead

Work is in progress to derive deconstruction patterns for records. It should present interesting use cases with ‘Sealed Types’ and ‘Pattern Matching’. A future version of Java should also see addition of methods to the Object class to enables reflections to work with Records.

In the meantime, why not take a look at my screencast on Java 14 language features:

Author
Mala Gupta
Mala Gupta works as a Developer Advocate with JetBrains. A Java Champion, she has over 19 years of experience in the software industry as an author, speaker, mentor, consultant, technology leader, and developer. She has been actively supporting Oracle Java certifications as a path to career advancement. Her Java books with Manning Publications, are top-rated for Oracle Certification around the globe. She co-leads Delhi's Java User Group. Director with Women Who Code Delhi, she drives initiatives for diversity advocacy for Women in Technology.

Leave a Reply

avatar
400