Java - Create Custom Record Constructor

This tutorial explains how to create a custom constructor for a record in Java.

Since Java 14, you can use record which is a special type of class with less ceremony. When creating a record, there will be a default constructor whose arguments are all the fields of the record. If you just want to set the passed arguments to the fields, it can be helpful since you don't need to declare a constructor. However, in some cases, you need to have a custom logic in the constructor. For example, if you want to check that a value must not be null or if you want to sanitize or format a value. Fortunately, Java's record allows you to create custom constructors. In addition, you can also create static named constructors. Below are the examples.

Create Custom Canonical Constructor

Let's start with a simple record that uses the default constructor.

  record Player(String name, Collection<Integer> scores, char grade) {}

  Player player1 = new Player(null, List.of(10, 30, 20), 'C');

We have a requirement that the name field must not be null. Another requirement is the scores field must have ordered values. To meet those requirements, we can create a custom constructor whose parameters are the same as the default one (the same as the fields defined in the bracket after the record name). That kind of constructor is called a canonical constructor. Inside the constructor, we can write our own logic.

  record Player(String name, Collection<Integer> scores, char grade) {

    Player(String name, Collection<Integer> scores, char grade) {
      if (name == null) {
        throw new AssertionError();
      }

      this.name = name;
      this.scores = scores.stream()
          .sorted()
          .collect(Collectors.toList());
      this.grade = grade;
    }
  }

Create Custom Non-Canonical Constructor

There is another case where the grade must be calculated from the passed scores. In this case the constructor only has two parameters which means it's different from the default constructor (or the field list) and called non-canonical constructor. For a non-canonical constructor, you need to delegate to another constructor. Otherwise, you'll get the following error.

  Non-canonical record constructor must delegate to another constructor.

Another rule is the first statement in the constructor body must call to this() (cannot declare any variable before). As a result, the code can be difficult to read especially if there's a complex logic to determine the value of a field.

  Player(String name, Collection<Integer> scores) {
    this(
        name,
        scores.stream()
            .sorted()
            .collect(Collectors.toList()),
        scores.stream()
            .mapToInt(Integer::intValue)
            .average().orElse(0) >= 80
                ? 'A'
                : scores.stream()
                    .mapToInt(Integer::intValue)
                    .average().orElse(0) >= 50
                        ? 'B' : 'C'
    );
  }

Create Custom Static Named Constructor

To overcome the downside of the previous example, we can create a custom static constructor. That makes it possible to create some variables and the code becomes more readable. A static constructor needs to return an instance of the record by calling another constructor.

  static Player createPlayer(String name, Collection<Integer> scores) {
    List<Integer> sortedScores = scores.stream()
        .sorted()
        .collect(Collectors.toList());
  
    double avgScore = scores.stream()
        .mapToInt(Integer::intValue)
        .average().orElse(0);
  
    char grade;
  
    if (avgScore >= 80) {
      grade = 'A';
    } else if (avgScore >= 50) {
      grade = 'B';
    } else {
      grade = 'C';
    }
  
    return new Player(
        name,
        sortedScores,
        grade
    );
  }

Another advantage of using static named constructor is you can create as many constructors as you want, each has a different name.

  static Player fromMap(Map<String, Object> map) {
    return new Player(
        (String) map.get("name"),
        (Collection<Integer>) map.get("scores"),
        (Character) map.get("grade")
    );
  }

Summary

If you need to write your own logic for constructing a record instance, you can create a custom constructor. If the constructor has the same field list as the record, you can override the default one. Otherwise, you have to create a non-canonical constructor or a static named constructor.

You can also read about: