Encapsulating Data with Records (Advanced)
Records, finalized in Java 16 (JEP 395) and fully supported in Java 21, generate boilerplate automatically. This lesson covers the customisation points, generic records, interface implementation, and serialization behaviour tested on the OCP exam.
The Canonical Constructor
The canonical constructor has one parameter for each record component, in declaration order. The compiler generates it automatically, but you can override it to add validation or normalization.
Explicit canonical constructor
javapublic record Range(int min, int max) {
public Range(int min, int max) { // same signature as components
if (min > max)
throw new IllegalArgumentException(
"min must be <= max, got: " + min + " > " + max);
this.min = min;
this.max = max;
}
}Compact canonical constructor (preferred for validation)
The compact form omits the parameter list — the compiler fills in the assignments automatically after the body runs:
javapublic record Range(int min, int max) {
public Range { // no parameter list
if (min > max)
throw new IllegalArgumentException(
"min must be <= max, got: " + min + " > " + max);
// min and max are assigned automatically after this block
}
}In the compact form, the parameters are the component names (
min,max) and you may reassign them before the implicitthis.min = minstep, for example to normalize data:min = Math.max(0, min);
Custom Constructors
A record may define additional constructors, but they must delegate to the canonical constructor as their first statement:
javapublic record Point(double x, double y) {
// Convenience constructor — origin
public Point() {
this(0.0, 0.0); // delegates to canonical
}
}Additional Methods
Records can define instance methods, static methods, and static fields. They cannot define instance fields beyond the declared components:
javapublic record Circle(double radius) {
// Static constant — allowed
public static final double PI = Math.PI;
// Instance method
public double area() {
return PI * radius * radius;
}
// Static factory
public static Circle unit() {
return new Circle(1.0);
}
// Override the generated accessor
@Override
public double radius() {
return Math.abs(radius); // normalize negative input
}
}Generic Records
Records support type parameters, making them useful for generic data containers:
javapublic record Pair<A, B>(A first, B second) {
public Pair<B, A> swap() {
return new Pair<>(second, first);
}
}
Pair<String, Integer> p = new Pair<>("hello", 42);
Pair<Integer, String> q = p.swap(); // (42, "hello")javapublic record Result<T>(T value, String error) {
public boolean isSuccess() { return error == null; }
public static <T> Result<T> ok(T value) {
return new Result<>(value, null);
}
public static <T> Result<T> fail(String error) {
return new Result<>(null, error);
}
}Records Implementing Interfaces
Records are final and already extend java.lang.Record, but they can implement any number of interfaces:
javapublic interface Printable {
void print();
}
public interface Measurable {
double measure();
}
public record Rectangle(double width, double height)
implements Printable, Measurable {
@Override
public void print() {
System.out.printf("Rectangle(%s x %s)%n", width, height);
}
@Override
public double measure() {
return width * height; // area
}
}Records also work seamlessly with sealed interfaces (see lesson 7-3):
javapublic sealed interface Shape permits Circle, Rectangle {}
public record Circle(double radius) implements Shape {}
public record Rectangle(double w, double h) implements Shape {}Serialization
Records implement java.io.Serializable when you declare implements Serializable:
javapublic record Employee(String name, int id)
implements java.io.Serializable {}Serialization of records uses the canonical constructor for deserialization — meaning validation in the canonical constructor is always re-run when the record is read back. This is safer than class-based serialization, which can bypass constructors entirely.
| Behaviour | Regular class serialization | Record serialization |
|---|---|---|
serialVersionUID |
Recommended to declare explicitly | Not used — structural compatibility is checked differently |
| Object creation on read | Bypasses constructors | Always calls canonical constructor |
| Validation on read | Must use readObject |
Automatic — canonical constructor runs |
What You Cannot Do with Records
| Restriction | Reason |
|---|---|
extends AnotherClass |
Records already extend java.lang.Record |
| Declare instance fields outside components | All state must be captured in components |
Make a record abstract |
Records are implicitly final |
Override equals, hashCode, toString with incompatible behaviour |
Allowed technically but strongly discouraged |
Key Rules to Remember
- The compact canonical constructor is the recommended way to add validation; assignments happen automatically after the block
- Additional constructors must delegate to the canonical constructor via
this(...) - Records can define instance methods, static methods, and static fields; no extra instance fields
- Generic records work exactly like generic classes
- Records implementing
Serializableuse the canonical constructor on deserialization — validation is enforced - Records are
finaland cannot extend other classes, but can implement any interfaces