Java Records (JEP 359)

Feb 03, 2020
Java Records (JEP 359)
Records are new type in Java 14, which allow you to declare simple data classes without all the boilerplate.

The problem

One of the issues with Java is its verbosity and the amount of boilerplate code needed. That's nothing new.

Let's consider a simple Cat class in Java. We want each cat to have:

  • Name
  • Number of lives
  • Color

Quite simple, right? Now let's look at the code in Java. For simplicity, let's make our class immutable - no setters, we'll set up everything in our constructor.


public final class Cat {
    
    private final String name;
    private final int numberOfLives;
    private final String color;

    public Cat(String name, int numberOfLives, String color) {
        this.name = name;
        this.numberOfLives = numberOfLives;
        this.color = color;
    }

    public String getName() {
        return name;
    }

    public int getNumberOfLives() {
        return numberOfLives;
    }

    public String getColor() {
        return color;
    }
}

It's already quite long, isn't it? It gets worse. We'll also want to have some basic implementation of equals() and hashCode().

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Cat cat = (Cat) o;
    return numberOfLives == cat.numberOfLives &&
            Objects.equals(name, cat.name) &&
            Objects.equals(color, cat.color);
}

@Override
public int hashCode() {
    return Objects.hash(name, numberOfLives, color);
}

Are we done yet? Not quite, we'll also need some nice toString() method:

@Override
public String toString() {
    return "Cat{" +
            "name='" + name + '\'' +
            ", numberOfLives=" + numberOfLives +
            ", color='" + color + '\'' +
            '}';
}

And we're finally done. It's about fifty lines of code! It's quite painful to write (although your IDE can help here) and difficult to read. What's worse, it's hard to find some extra functionality (such as new methods) in all the boilerplate.

In these fifty lines, there are only three lines that are actually interesting and bear some information:

private final String name;
private final int numberOfLives;
private final String color;

The rest is just boilerplate, which is predictable and can be automatically generated based on these three lines. Your IDE can do that, and there are tools such as Lombok, which can do this for you as well.

In Java, you often use classes, which just hold data, like our Cat. The implementation is always pretty much the same - a bunch of fields, getters, equals(), hashCode() and toString(). Often it is useful to have them immutable, if possible, which has many benefits. But to write and read such classes is a lot of work as there is a lot of code involved. And it is error-prone. Who knows whether your hashCode() and equals() code is actually correct?

Records

Java 14 tries to solve this issue by introducing a new type called Record, it is described by JEP 359: Records (Preview)

The same 50 lines long class from the example above could be written as a record like this:

public record Cat(String name, int numberOfLives, String color) { }

It's a lot less code, right?

The functionality is the same as in our previous example - we have:

  • an immutable class with three fields
  • Constructor assigning these fields
  • Getters
  • equals(), hashCode() and toString()

To illustrate this better, let's look at the decompiled version of our record.

public final class Cat extends java.lang.Record {
    private final java.lang.String name;
    private final int numberOfLives;
    private final java.lang.String color;

    public Cat(java.lang.String name, int numberOfLives, java.lang.String color) { /* compiled code */ }

    public java.lang.String toString() { /* compiled code */ }

    public final int hashCode() { /* compiled code */ }

    public final boolean equals(java.lang.Object o) { /* compiled code */ }

    public java.lang.String name() { /* compiled code */ }

    public int numberOfLives() { /* compiled code */ }

    public java.lang.String color() { /* compiled code */ }
}

You can see that the code is pretty much the same as our old Cat. One notable exception is that getters for the fields generated are not named as usual - instead of getColor(), there is just color().

Also, the class extends java.lang.Record.

The equals() implementation considers two records to be equal if they are the same Type and have the same values. The toString() implementation prints our record like this:

Cat[name=Fluffy, numberOfLives=9, color=White]

Even though these methods are automatically provided for you, it is possible to override them if necessary.

Limitations

There are some restrictions and limitations of records, which you should be aware of.

  • Records cannot extend any class, although they can implement interfaces
  • Records cannot be abstract
  • Records are implicitly final; they cannot be inherited from
  • You can declare additional fields in the body of a record, but only if they are static

Adding methods

Even though records are mostly used just as plain data carriers, you can declare your own methods. Of course, since records are immutable, you cannot change any state, but it can still be useful. For example:

public record Cat(String name, int numberOfLives, String color) {
 
    public boolean isAlive() {
        return numberOfLives >= 0;
    }

}

You can also add static methods.

Custom constructors

By default, new records contain only a constructor, which requires all the fields of the record as parameters. For example, our cat, which has three fields, needs to be constructed like this:

Cat cat = new Cat("Fluffy", 9, "White");

What if some parameters can be optional - if we don't provide them, we can use some default value.

In our case, the number of lives for new cats is likely to be always 9. We can create an additional constructor, which accepts only name and color, and the number of lives can be set to 9 as a default value. Of course, the constructor with all three fields still exists and is available.

public record Cat(String name, int numberOfLives, String color) {

    public Cat(String name, String color) {
        this(name, 9, color);
    }
}

The all-fields constructor is automatically generated for us. But sometimes you need to perform some custom logic there. Such as input validation. You can declare the all-fields constructor by yourself if you need to:

public record Cat(String name, int numberOfLives, String color) {

    public Cat(String name,int numberOfLives, String color) {
        if(numberOfLives < 0) {
            throw new IllegalArgumentException("Number of lives cannot be less than 0.");
        }

        if(numberOfLives > 9) {
            throw new IllegalArgumentException("Cats cannot have that many lives.");
        }

        this.name = name;
        this.numberOfLives = numberOfLives;
        this.color = color;
    }
}

If you are overriding the constructor with all the fields which record specifies (canonical constructor), you can use a declaration without writing the parameters. They are still available for use, but the code is shorter.

public record Cat(String name, int numberOfLives, String color) {

    // This is the same as public Cat(String name, int numberOfLives, String color)
    public Cat {
        // name, numberOfLives and color available here
    }
}

Runtime introspection

There are two new methods added to java.lang.Class, which have records-related functionality.

The first one is called isRecord(). It is pretty straightforward, you can just check if something is a record or not:

Cat cat = new Cat("Fluffy", 9, "White");
if(cat.getClass().isRecord()) {
    //...
}

The other one is getRecordComponents(). You would call it in the same way as in the example above. It returns a list of java.lang.reflect.RecordComponent. It is basically a list of all the fields, which are in the record with information such as:

  • Name
  • Type
  • Accessor
  • Annotations

Also new in Java 14




Let's connect