Back to Blog
Java

Java Record Constructor: Canonical and Compact Forms

java record constructor: How Java record constructors work: the implicit canonical constructor, compact constructor syntax for validation, secondary constructors, and...

Java recordscompact constructorcanonical constructorJava 16immutable objectsvalidation
Illustration of a Java record constructor showing a compact constructor validating input before implicit field assignment.

When you declare a Java record, the compiler generates a canonical constructor that accepts one parameter for each record component. This implicit java record constructor is what makes a record usable without any explicit constructor declaration:

public record Point(int x, int y) {}

The generated constructor assigns each component field directly from the corresponding parameter. There is no validation, no copying, and no transformation. For many records, that default behavior is exactly what you want. When it is not, you can replace or extend the constructor with one of two forms: a compact constructor or a secondary constructor.

The Compact Constructor Syntax

A compact constructor lets you write a constructor body without repeating the parameter list. The parameters are implicit, and you refer to component names directly:

public record Temperature(double celsius) { public Temperature { if (celsius < -273.15) { throw new IllegalArgumentException("Temperature cannot be below absolute zero"); } } }

The body runs before the compiler-generated field assignments. After the body completes, each parameter value is assigned to the corresponding component field. This is why the compact constructor is the natural place for validation: any exception thrown here prevents the record from being constructed.

The compact constructor is not a separate constructor in the bytecode. It compiles to the same canonical constructor that the compiler would have generated, with your body inserted before the field assignments.

Normalizing Values Before Assignment

Because the assignment happens after the body, you can reassign the parameters to normalize the stored values:

public record NormalizedText(String text) { public NormalizedText { text = text == null ? "" : text.trim(); } }

Here the parameter text is reassigned. The final value—after null handling and trimming—is what gets stored in the component field. This pattern is useful when a record should enforce a canonical form for its data, such as trimming whitespace or converting to a consistent case.

You cannot assign to the component fields directly in a compact constructor. The fields are final, and the compiler generates the assignment after your body. Attempting this.text = text is a compile error. Reassigning the parameter is the intended mechanism.

Adding Secondary Constructors

Records can declare additional constructors beyond the canonical one. A secondary constructor must delegate to another constructor as its first statement:

public record Color(int red, int green, int blue) { public Color { if (red < 0 || red > 255 || green < 0 || green > 255 || blue < 0 || blue > 255) { throw new IllegalArgumentException("RGB values must be between 0 and 255"); } } public Color(String hex) { this( Integer.parseInt(hex.substring(1, 3), 16), Integer.parseInt(hex.substring(3, 5), 16), Integer.parseInt(hex.substring(5, 7), 16) ); } }

The secondary constructor parses a hex string and delegates to the canonical constructor. Because delegation goes through the canonical constructor, the validation in the compact body runs for every construction path. This keeps the invariants in one place.

Defensive Copying in the Constructor

Records do not perform defensive copying of mutable components. If a component is an array or a collection, the reference is stored as-is. A compact constructor is the right place to copy the value before it is stored:

public record ByteBuffer(byte[] data) { public ByteBuffer { data = data.clone(); } }

This prevents the record from observing later mutations of the original array. Note that the accessor method still returns the internal array reference, so callers can mutate the record's state through the getter. If that is a concern, you can override the accessor to return a copy as well.

Runtime Behavior and the Canonical Constructor

At the bytecode level, there is only one canonical constructor per record. The compact constructor body is inlined into it, and the field assignments follow. This means a compact constructor has no additional runtime cost compared to a hand-written canonical constructor—the generated bytecode is equivalent.

The canonical constructor is also what reflection-based tools use. Frameworks that construct records reflectively, such as serialization libraries, look up the constructor whose parameter types match the record components. If you add validation in a compact constructor, that validation runs even when the record is created reflectively.

Common Mistakes and Their Causes

A frequent mistake is trying to delegate from a compact constructor using this(...). The compact constructor is the canonical constructor; it cannot delegate to itself. If you need an additional construction path, declare a secondary constructor that delegates to the canonical one.

Another mistake is writing explicit field assignments in the compact body. The compiler generates those assignments after the body, and assigning to the final fields yourself is a compile error. Reassign the parameters instead.

A third mistake is assuming that validation in the constructor protects against all invalid states. Records guarantee that the constructor runs, but if a component is mutable and the accessor exposes the internal reference, the record's state can still change after construction. Defensive copying in the constructor addresses the input side; overriding the accessor addresses the output side.

java record constructor: Practical Usage and Code Examples | RYUSLOG DEV