This rule raises an issue when a record component accessor is called on the record being constructed, inside a compact constructor or inside an explicit canonical constructor before the corresponding field is assigned.

Why is this an issue?

The fields of a record are assigned only once, by its canonical constructor:

Until a field is assigned, it holds the default value of its type: null, 0, or false. Reading such a field through this. is rejected by the compiler, but calling the component accessor, for example name() or this.name(), compiles and silently reads the field.

As a consequence, the accessor returns the default value instead of the value passed to the constructor. This typically defeats the validation performed in the constructor: a check such as name().isBlank() throws a NullPointerException instead of validating the argument, and a check such as age() < 0 is always false, so invalid values are accepted. The same applies to explicitly declared accessors, as they typically read the same unassigned field.

Calls placed in lambdas and in local or anonymous classes are not reported, as they may run after the construction of the record.

How to fix it

Read the constructor parameter instead of calling the accessor. In a compact constructor, the parameters have the same names as the record components. In an explicit canonical constructor, either read the parameter or call the accessor only after the field has been assigned.

Code examples

Noncompliant code example

record User(String name, int age) {
  User {
    if (name().isBlank()) { // Noncompliant: name() returns null here
      throw new IllegalArgumentException("Name must not be blank");
    }
    if (age() < 0) { // Noncompliant: age() always returns 0 here
      throw new IllegalArgumentException("Age must not be negative");
    }
  }
}

Compliant solution

record User(String name, int age) {
  User {
    if (name.isBlank()) {
      throw new IllegalArgumentException("Name must not be blank");
    }
    if (age < 0) {
      throw new IllegalArgumentException("Age must not be negative");
    }
  }
}

Noncompliant code example

record Email(String address) {
  Email(String address) {
    if (!address().contains("@")) { // Noncompliant: address() returns null here
      throw new IllegalArgumentException("Invalid email address");
    }
    this.address = address.toLowerCase(Locale.ROOT);
  }
}

Compliant solution

record Email(String address) {
  Email(String address) {
    if (!address.contains("@")) {
      throw new IllegalArgumentException("Invalid email address");
    }
    this.address = address.toLowerCase(Locale.ROOT);
  }
}

Resources

Documentation

Related rules