Sections in this lesson
Java Records
Use value-oriented data carriers while validating invariants and avoiding mutable aliases.
Reviewed: · Content owner: Interview Preparator
What you will learn
- Explain generated accessors, equality and constructors.
- Use compact constructors for validation and normalization.
- Distinguish shallow immutability from deep immutability.
Before you start
The flow
- 1.Accept components
- 2.Validate
- 3.Copy
- 4.Construct value
- 5.Compare
What the declaration promises
A record declares its state as components. The compiler supplies component accessors, a canonical constructor, equals, hashCode and toString. Accessors use the component name, such as topics(), rather than a JavaBean getter. Records are final and can implement interfaces.
Generated equality is based on component equality. An array component still uses array reference equality by default; a record does not magically add content comparison. Avoid exposing secrets through generated toString.
Defensive copying matters
Final fields prevent reassignment, not mutation of the referenced object. Copy an input collection in the compact constructor and return an unmodifiable representation. List.copyOf prevents list edits, but does not deeply copy mutable elements.
The example checks both mutation of the original list and edits through the accessor. Constructor validation keeps every created value inside the contract, including values reconstructed by a serializer.
Where a record fits
Use records for request/response values, projections and immutable domain values. A persistence entity with identity, lazy proxies and a mutable lifecycle often needs a different model. Keep wire DTOs separate from persistence and domain concerns when their invariants differ.
Try it
import java.util.ArrayList;import java.util.List;public class Records { record StudyPlan(String name, List<String> topics) { StudyPlan { if (name == null || name.isBlank()) { throw new IllegalArgumentException("Name is required"); } name = name.strip(); topics = List.copyOf(topics); } } public static void main(String[] args) { var source = new ArrayList<>(List.of("Java")); var plan = new StudyPlan(" Backend ", source); source.add("Spring"); if (plan.topics().size() != 1) throw new AssertionError("Mutable alias leaked"); if (!plan.equals(new StudyPlan("Backend", List.of("Java")))) { throw new AssertionError("Value equality failed"); } try { plan.topics().add("AI"); throw new AssertionError("Exposed list must be unmodifiable"); } catch (UnsupportedOperationException expected) { System.out.println(plan); } try { new StudyPlan(" ", List.of()); throw new AssertionError("Blank name accepted"); } catch (IllegalArgumentException expected) { System.out.println("Constructor validation passed."); } }}Run npm run examples:java with JDK 21+. This file demonstrates constructor invariants, list aliasing and value equality.
Interview questions
Are records deeply immutable?
No. Component references are final; referenced collections and their elements can still be mutable.
What does a compact constructor change?
It validates or normalizes canonical constructor parameters before implicit component assignments.
Would you use a record for every JPA entity?
No. Evaluate entity identity, proxy requirements and lifecycle against the immutable value contract.
Practice
Add mutable topic objects to the plan and demonstrate why copying only the list is insufficient.
Check your solution
- Original-list mutation does not change the plan.
- Blank names fail construction.
- The difference between shallow and deep copying is demonstrated.
Official references
Continue learning
Category sequence
Java
Lesson 41 / 42
Was this lesson useful?
Change notes: Phase 2: bilingual lesson with versioned executable reference.