Watercooler
March 22, 2021
5
min read

Modern Java Records

Michael Inden

In this blog, I will present Records, one of my favorite features of modern Java.

Records represent a simplified form of classes whose methods are implicitly derived from the attributes defined as constructor parameters. A record constitutes a collection of data and models only exactly one immutable state (however, immutability is only guaranteed if only primitive data types are used for the con structor parameters or only immutable types are specified). In addition, implementations of read-only methods for attribute access as well as equals() and hashCode() are automatically generated in a contract-compliant manner, as well as a constructor and toString(). 

As a supplementary analogy, the mathematical concept of tuples can be used, i.e., a grouping of several values of potentially different types. 

All in all, records are profitably useful in various use cases. Let’s take a look at an introductory example and then at more advanced possibilities.

Introductory example

The introductory explanations may still sound a bit complicated. Still, after a look at the following source code, the picture will become clearer: 

record MyPoint(int x, int y) { }

Let's consider how much source code would have to be written to achieve equivalent functionality using the existing Java language facilities: 

public final class MyPoint
{
  private final int x;
  private final int y;

  public MyPoint(int x, int y)
  {
    this.x = x;
    this.y = y;
  }

  @Override
  public boolean equals(Object o)
  {
    if (this == o)
    return true;
    if (o == null || getClass() != o.getClass())
    return false;
    MyPoint point = (MyPoint) o;
    return x == point.x && y == point.y;
  }

  @Override
  public int hashCode()
  {
    return Objects.hash(x, y);
  }

  @Override
  public String toString()
  {
    return "MyPoint[x=" + x + ", y=" + y + "]";
  }

  // nur lesende Zugriffsmethoden auf x und y
  public int x()
  {
    return x;
  }
  public int y()
  {
    return y;
  }
}

It is also quite interesting to use the tool javap to get the generated bytecode in the form of an overview: 

$ javap MyPoint
Compiled from "MyPoint.java"

final class java14.MyPoint extends java.lang.Record {
  public java14.MyPoint(int, int);
  public int x();
  public int y();
  public java.lang.String toString();
  public int hashCode();
  public boolean equals(java.lang.Object);
}

It is obvious that records possess the base type java.lang.Record. Furthermore, suitable access methods are created for each constructor argument. As seen above, their naming does not adhere to JavaBeans standard (has no prefix get) but consists only of the attribute name.

Enhancements 

Enhancements – definition of own methods 

So far, records are very intuitive, and it stays that way—even if we want to add our own methods there. Let’s take the following record PersonDTO as an example:

record PersonDTO(String firstname, String lastname, LocalDate birthday) { }

We now want to implement the output of the full name as a concatenation of first and last name in the form of a method asFullname(): 

record PersonDTO(String firstname, String lastname, LocalDate birthday) {
  public String asFullname()
  {
    return firstname + " " + lastname;
  }
}

Let's take a look at this for the definition above: 

final PersonDTO micha = new PersonDTO("Michael", "Inden", LocalDate.of(1971, 2, 7));
System.out.println(micha);
System.out.println(micha.asFullname());

These lines produce the following output: 

PersonDTO[firstname=Michael, lastname=Inden, birthday=1971-02-07] Michael Inden

If really necessary in rare cases, even the automatically generated three methods equals(), hashCode() and toString() can be overridden, e.g., for the initial example as follows: 

record MyPoint(int x, int y)
{
  @Override
  public String toString()
  {
    return String.format("(x: %d,y: %d)", x, y);
  }
}

In the Listing above, we see another special feature: In the record itself, we can, of course, address the attributes directly by name.

Enhancement – Definition of own constructors 

Besides defining your own methods, you can also specify your own constructors. Let’s assume we want to be able to construct our record MyPoint from a textual representation in the format x,y as well. Therefore we implement the following constructor:

record MyPoint(int x, int y)
{
  public MyPoint(String values)
  {
    this(Integer.parseInt(values.split(",")[0]),
    Integer.parseInt(values.split(",")[1]));
  }
}

This then allows the following construction: 

MyPoint myPoint = new MyPoint("72,71");

Special operations – validity checks of the parameters 

When defining a record, it is sometimes important to check certain boundary conditions or value ranges for some parameters. For such cases, one can explicitly specify a special constructor in the definition of the record. 

Let's consider the entire thing for defining ranges of values of a closed interval, where the lower bound must always be less than or equal to the upper bound. Let’s implement this with a record:

record ClosedInterval(int lower, int upper)
{
  public ClosedInterval(int lower, int upper)
  {
    if (lower > upper)
    {
      String errorMsg = String.format("invalid: %d (lower) > %d (upper)", lower, upper);
      throw new IllegalArgumentException(errorMsg);
    }

    this.lower = lower;
    this.upper = upper;
  }
}

Interestingly, there is a syntactic characteristic: You can also use the following construct instead of a constructor – in this way, the same checks are made, but you do not have to assign the parameters to the attributes by hand: 

record ClosedInterval(int lower, int upper)
{
  public ClosedInterval
  {
    if (lower > upper)
    {
      String errorMsg = String.format("invalid: %d (lower) > %d (upper)", lower, upper);
      throw new IllegalArgumentException(errorMsg);
    }
  }
}

Conclusion 

The Java releases up to and including 13 are rather manageable in terms of their innovations. This is true even for Java 11 as an LTS version. Fortunately, Java 14 brings a good slew of useful enhancements: On the one hand, there are the convenient syntax changes in switch. These are covered in the previous blog. 

On the other hand, there is one more outstanding new feature: the records. These allow the implementation of Immutable classes with minimal writing effort. Records simplify, for example, DTO classes' definition, parameter value objects, or tuples for combined return types. Once used, you will love it and never want to miss this new feature.

Modern Java Records

March 22, 2021
5
min read

Subscribe to DevDigest

Get a weekly, curated and easy to digest email with everything that matters in the developer world.

Learn more

From developers. For developers.