Model Data with Records

final class Coordinates {

  private final double latitude;
  private final double longitude;

  Coordinates(double latitude, double longitude) {
    if (latitude < -90 || latitude > 90) {
      throw new IllegalArgumentException("bad latitude");
    }
    this.latitude = latitude;
    this.longitude = longitude;
  }

  double getLatitude() {
    return latitude;
  }

  double getLongitude() {
    return longitude;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (!(o instanceof Coordinates)) {
      return false;
    }
    Coordinates that = (Coordinates) o;
    return Double.compare(latitude, that.latitude) == 0
        && Double.compare(longitude, that.longitude) == 0;
  }

  @Override
  public int hashCode() {
    return Objects.hash(latitude, longitude);
  }

  @Override
  public String toString() {
    return "Coordinates[latitude=" + latitude + ", longitude=" + longitude + "]";
  }
}

Coordinates is a plain data carrier with two fields, yet the class is more than 40 lines long. The constructor, the getters, equals(), hashCode(), and toString() are all boilerplate, the kind of code an IDE generates and nobody reads.

It's also a maintenance trap. Every new field means touching five places, and forgetting one, say in equals(), is a silent bug. Meanwhile, the one interesting line, the range check of the latitude, is hard to spot among all the noise.

record Coordinates(double latitude, double longitude) {

  Coordinates {
    if (latitude < -90 || latitude > 90) {
      throw new IllegalArgumentException("bad latitude");
    }
  }
}

Since Java 16, a record declares its components once, in the header. The compiler generates the constructor, the accessors, equals(), hashCode(), and toString(). Records are immutable: all fields are private final, and there are no setters.

The compact constructor keeps the validation, which is now the only code left to read. Note that the accessors are called latitude() instead of getLatitude(), so callers need a small change.

Use records for values whose identity is their data, such as coordinates, amounts, or IDs. For entities with mutable state, a regular class remains the better choice.