Java Records Explained: Compact Constructors and Immutability
A Java record is a one-line immutable data class that generates its constructor, accessors, equals, hashCode and toString. Compact constructors and the rules.
- Course: Java study plan
- Module: Enums & records
- Kind: Lesson
- Reading time: 14 min
- Runtime: OpenJDK 18
What is a record in Java?
A record in Java, standard since Java 16, is a class declared as a transparent, immutable carrier of data: record Point(int x, int y) {}. From that header the compiler generates a private final field per component, a canonical constructor, accessors named x() and y(), and equals, hashCode and toString over all components. Records are final and cannot extend another class.
Lesson
Most classes that hold data — a point, a pair, a DTO, a parse result — need the same forty lines: private final fields, a constructor, accessors, equals, hashCode, toString. Java 16's records generate all of it from one line, and in doing so make a promise the compiler enforces: a record is a transparent, immutable carrier of its components. This lesson covers what a record generates, what you can customise, and the rules that keep the promise.
Declaring a record
public record Point(int x, int y) { }
Point p = new Point(3, 4);
p.x() // 3 — accessor, named after the component (no "get")
p.y() // 4
p.toString() // "Point[x=3, y=4]"
p.equals(new Point(3, 4)) // true — component-wise
p.hashCode() // combined from x and y, consistent with equals
The header (int x, int y) declares the components. From it the compiler generates:
- a
private finalfield per component; - a canonical constructor taking all components in order;
- a public accessor per component, named exactly like it;
equals,hashCodeandtoStringover all components.
Records are implicitly final, extend java.lang.Record (so cannot extend anything else), and may implement interfaces. They can be top-level, nested (implicitly static) or local — declared inside a method, handy for a temporary pair in an algorithm.
What you can add
public record Range(int lo, int hi) implements Comparable<Range> {
// compact canonical constructor: validate / normalise; assignment is implicit at the end
public Range {
if (lo > hi) throw new IllegalArgumentException(lo + " > " + hi);
}
// extra constructors must delegate to the canonical one
public Range(int single) { this(single, single); }
// instance methods
public int length() { return hi - lo; }
public boolean contains(int x) { return lo <= x && x < hi; }
// static members and factories
public static Range empty() { return new Range(0, 0); }
public static final Range UNIT = new Range(0, 1);
// an accessor may be overridden (rarely wise); its signature is fixed
@Override public int lo() { return lo; }
@Override public int compareTo(Range o) { return Integer.compare(lo, o.lo); }
}
The compact constructor has no parameter list; it runs before the fields are assigned and may reassign the parameters (lo = Math.min(lo, hi)) to normalise them. It cannot assign this.lo directly — the assignment happens automatically after the body.
What you cannot do
- No instance fields beyond the components.
private int cache;in a record is a compile error. Static fields are fine. - No setters, no mutation of components: the fields are final. "Modifying" means creating a new record:
new Point(p.x() + 1, p.y())— awithX(int)method is the idiom if you want it. - No
extends; records may not be abstract; components cannot bevolatile/transientin the usual sense. - Accessor names are fixed to the component names; a
getX()alongside is legal but pointless.
Equality is by value, always
equals compares each component with == for primitives (Double.compare semantics for float/double) and equals for references. Two records with equal components are equal — which is what a data carrier should mean, and what makes records good map keys and set elements without any hand-written code. Caveat: an array component uses Object.equals on the array (identity), so record Bag(int[] items) compares arrays by reference — use a List instead.
Records and collections: shallow immutability
public record Team(String name, List<String> members) {
public Team {
members = List.copyOf(members); // defensive, immutable copy in the compact constructor
}
}
A record's fields are final; the objects they refer to are whatever they are. A List component can be mutated by whoever holds the list unless you copy it on the way in. List.copyOf produces an immutable copy (and rejects nulls); for maps Map.copyOf. Make this a habit for every collection or array component.
Records as tuples and multi-value returns
record MinMax(int min, int max) { }
static MinMax minMax(int[] a) { … return new MinMax(lo, hi); }
MinMax r = minMax(values);
r.min(); r.max();
The answer to "how do I return two values" (Module 5) is a record — named, typed, and one line to declare, even locally inside the method that needs it.
Records with sealed interfaces
sealed interface Shape permits Circle, Square { }
record Circle(double r) implements Shape { }
record Square(double side) implements Shape { }
Records are the natural leaves of a sealed hierarchy (Module 9): final, immutable, with generated equality. Java 21's record patterns destructure them in a switch: case Circle(double r) -> Math.PI * r * r.
Records versus classes
| Need | Use |
|---|---|
| Immutable data with value equality | record |
| Mutable state, identity, lifecycle | class |
| Inheritance from a class | class (records cannot extend) |
| Extra hidden state or caching fields | class (records forbid instance fields) |
| JPA entities, JavaBeans frameworks needing setters | class |
| DTOs, keys, tuples, messages, configuration values | record |
Records are not "classes with less typing"; they are a semantic declaration that the type is nothing but its components. If that is not true — if two instances with equal components should be distinguishable, or the state changes — it is not a record.
Serialization, reflection and frameworks
Records serialise through their canonical constructor (safer than field-poking), and Class.getRecordComponents() exposes the components to frameworks. Jackson, Gson and most modern libraries support records directly. Older frameworks that require a no-arg constructor and setters do not.
Interview angle
- "What does a record generate?" Final fields, canonical constructor, accessors,
equals/hashCode/toString. - "Can a record have a mutable field?" No instance fields besides the components, all final; the referenced objects may be mutable — copy them in.
- "Can a record extend a class?" No; it can implement interfaces.
- "What is a compact constructor?" The canonical constructor without a parameter list, for validation and normalisation before the implicit assignments.
- "Are records immutable?" Shallowly: components cannot be reassigned; deep immutability depends on the component types.
Key takeaways
record Name(T a, U b)generates fields, constructor, accessorsa()/b(), and value-basedequals/hashCode/toString.- Customise with a compact constructor (validate, normalise), extra constructors delegating to the canonical one, methods, statics, interfaces.
- No extra instance fields, no setters, no
extends; equality is by components — array components compare by reference. - Copy collection components defensively (
List.copyOf) for real immutability. - Use records for data; classes for identity and mutation.
Common questions
What is a compact constructor in a Java record?
A compact constructor is the record's canonical constructor written without a parameter list, as in public Range { … }. It runs before the fields are assigned, so it can validate the parameters, throw on bad input or reassign them to normalise values; the fields are then set automatically from the parameters.
Are Java records immutable?
Shallowly. A record's fields are final and it has no setters, but a component that refers to a mutable object, such as a List, can still be changed by whoever holds that object. Copy such components in the compact constructor with List.copyOf for real immutability.
Can a record extend a class in Java?
No. Every record implicitly extends java.lang.Record and is final, so it can neither extend another class nor be extended. A record can implement any number of interfaces, which is how it joins a sealed hierarchy or becomes Comparable.
What is the difference between a record and a class in Java?
A record declares that a type is nothing but its components: immutable, compared by value, with everything generated. Use a class when the type has identity, mutable state, hidden fields such as a cache, a superclass, or a framework that needs a no-argument constructor and setters.
Can a record have instance fields in Java?
No — only the fields generated from its components. Declaring another instance field is a compile error, though static fields, static methods, instance methods and extra constructors that delegate to the canonical one are all allowed.
Exercises
Points as records
Declare record Point(int x, int y) with a method double distanceTo(Point o) and a static factory origin(). Read an integer n and n pairs x y. Put every point into a HashSet<Point> and print distinct=<size>; print hasOrigin=<set contains Point.origin()>; then print each distinct point in input order (use a LinkedHashSet) as its toString() followed by its distance to the origin with two decimals.
Example: 3 then 3 4, 0 0, 3 4 →
distinct=2
hasOrigin=true
Point[x=3, y=4] 5.00
Point[x=0, y=0] 0.00Rational numbers as records
Write record Rational(long num, long den) whose compact constructor rejects a zero denominator (IllegalArgumentException), moves any sign to the numerator and reduces by the gcd — so new Rational(2, -4) becomes -1/2. Add plus and times returning new records, and toString as num/den (just num when den == 1). Read n lines a b op c d and print a/b op c/d = <result>; record equality then works by value: also print equal=<result.equals(new Rational(…))> where the second is the same fraction unreduced, given on the line as e f.
Example: 1 2 + 1 3 5 6 (one line, n = 1) →
1/2 + 1/3 = 5/6 equal=trueIn this module: Enums & records
← EnumSet and EnumMap · Records in practice — validation, wither methods and modelling →