Java Compact Constructor: Syntax and Use Cases
java compact constructor: Learn how to use Java compact constructors in records for validation and normalization, with syntax examples and practical guidance.
What Is a Compact Constructor in a Java Record?
When you declare a Java record, the compiler generates a canonical constructor that takes all record components as parameters and assigns them to the corresponding private final fields. The java compact constructor provides a concise way to add validation or normalization to that constructor without writing the full parameter list. For many records, the default behavior is sufficient, but when you need to enforce invariants or transform input values, the compact constructor is the cleanest option.
A compact constructor has the same name as the record, no parameter list, and no explicit assignment statements. The assignments happen implicitly after the constructor body runs. This lets you add logic without repeating the field assignments.
Compact Constructor Syntax and Rules
A compact constructor is declared inside the record body, with the same name as the record, but without parentheses. Inside the body, you can refer to the record components directly. After the body completes, the compiler assigns the (possibly modified) component values to the fields. Here is a minimal example:
public record Person(String name, int age) { public Person { if (age < 0) { throw new IllegalArgumentException("Age cannot be negative"); } } }
In this compact constructor, age is the parameter, and you can modify it if needed. The implicit assignment happens after the body. You cannot assign to this.name or this.age; those assignments are generated. You also cannot declare local variables that shadow the component names.
The compact constructor must have the same access modifier as the record itself. If the record is public, the compact constructor must be public. If the record is package-private, the compact constructor can be package-private.
Using Compact Constructors for Validation
Validation is the most common use case. Because the compact constructor runs before the fields are assigned, you can check the incoming values and throw exceptions early. This ensures that an invalid record instance cannot be created. For example:
public record Email(String address) { public Email { if (address == null || !address.contains("@")) { throw new IllegalArgumentException("Invalid email address"); } } }
The validation logic is centralized in the record definition, so every call to the constructor goes through the same checks. This is more reliable than validating in each factory method or static factory, because the compact constructor is the only way to instantiate the record (apart from deserialization, which has its own considerations).
Normalizing Data in a Compact Constructor
You can also transform the input values before they are stored. For instance, you might want to trim whitespace or convert a string to lowercase. Inside the compact constructor, you can reassign to the component parameter:
public record User(String username) { public User { username = username.trim().toLowerCase(); } }
Here, username is the parameter, and reassigning it updates the value that will be assigned to the field. This is a concise way to normalize data without writing a static factory. Note that the reassignment must happen before the implicit assignment, so you cannot read the field this.username inside the compact constructor; you only have the parameter.
Common Mistakes and Edge Cases
One common mistake is trying to assign to this.field inside the compact constructor. That is not allowed because the fields are final and the assignment is generated after the body. Another mistake is declaring a local variable with the same name as a component, which causes a compile-time error.
Another edge case: if you need to copy a record, the compact constructor is not called automatically. The copy is done via the canonical constructor, so if you want to enforce invariants on copies, you need to use a copy method that calls the constructor. For example:
public record Point(int x, int y) { public Point { if (x < 0 || y < 0) { throw new IllegalArgumentException("Coordinates must be non-negative"); } } public Point copy() { return new Point(x, y); } }
The compact constructor also does not affect the equals, hashCode, or toString methods generated by the record.
Compact Constructors and Maintainability
Keeping validation and normalization inside the compact constructor makes the record self-contained. Other developers can see the invariants directly in the record definition, which improves maintainability. However, if the validation logic becomes complex, it might be better to extract it into a separate method or a utility class to keep the constructor readable. The compact constructor should be short and focused; if it grows too large, consider using a static factory with explicit validation.
When to Avoid Compact Constructors
There are cases where a compact constructor is not the best choice. If you need to perform validation that depends on multiple components in a way that requires a different order of operations, or if you need to throw a specific exception type that requires additional context, you might prefer a full canonical constructor. Also, if you need to support deserialization with custom logic, the compact constructor is not used during deserialization; you would need to implement readObject or use a custom deserialization approach. For most records, however, the compact constructor is the cleanest way to enforce invariants.