A record declares a fixed set of components and supplies component accessors, construction, value-based equality, hashing, and a textual representation.
Java records: value carriers and defensive copies
Java 17+. Use a JDK that supports this release.
Validate at construction
A receipt batch should always have an ID and an owned list of receipt IDs. The compact constructor rejects a blank batch ID and copies the incoming list. Its accessor returns that copied, unmodifiable list.
Final component references do not freeze the objects they reference. List.copyOf prevents structural changes to this copied list; it does not copy a mutable object stored as an element. String elements make the ownership rule straightforward here.
Records are suitable when their component list is the value contract. They are less suitable when callers should never observe internal components or when an object needs an evolving identity separate from its data.
Equality follows the components
Two instances with equal components compare equal by the generated equals implementation. For an array component, array equality remains reference equality unless you override the relevant behavior. A record does not turn an array into a value-equality list.
Do not store sensitive components and then casually log the generated toString. The generated representation exposes component values. Decide redaction at the logging boundary.
The lesson targets Java 17 even though records became permanent earlier. That baseline lets the same project use the sealed-type lesson without preview flags.
Working program
import java.util.ArrayList;
import java.util.List;
public class ReceiptBatches {
record ReceiptBatch(String id, List<String> receiptIds) {
ReceiptBatch {
if (id == null || id.isBlank()) throw new IllegalArgumentException("Batch ID required");
receiptIds = List.copyOf(receiptIds);
}
}
public static void main(String[] args) {
List<String> imported = new ArrayList<>(List.of("r-41", "r-52"));
ReceiptBatch batch = new ReceiptBatch("batch-8", imported);
imported.clear();
System.out.println(batch.receiptIds().size());
System.out.println(batch.equals(new ReceiptBatch("batch-8", List.of("r-41", "r-52"))));
}
}Output
2
trueCost and design choices
Copying n list references takes O(n) time and O(n) storage. The immutable String elements are shared. Value equality for two lists can require O(n) element comparisons; a record is not guaranteed constant-time equality.
List.copyOf rejects null elements. If null has a domain meaning, choose a different representation and state that contract instead of silently dropping values.
Common Mistakes
- Do not claim records are deeply immutable.
- Do not assume array components use content equality.
- Do not log every generated component representation without checking data sensitivity.
Connect the contracts
Compare the boundary explained in Value equality with the assumptions made by this program.
Compare the boundary explained in Ownership boundaries with the assumptions made by this program.
Apply this contract in Spring
Spring Boot configuration properties: bind values and reject bad startup input, Spring MVC request validation: reject invalid commands before mutation. These lessons keep framework assembly separate from the Java contract.
Extend the tested workflow
Continue with Java records with arrays: copy on input and output, Spring Data JPA projections: return selected fields without a full entity.
Compare the Python boundary
Python dataclasses: frozen fields require an immutable value model.
