Records
Records are a concise way to declare classes that are transparent, immutable carriers for a fixed set of data.
Introduced as a preview feature in Java 14 and finalized in Java 16, records let you declare a data-carrier class in one line instead of writing a constructor, accessors, equals(), hashCode(), and toString() by hand. A record states its data — its "components" — and the compiler generates the boilerplate around it.
What Is a Record
A record is a special kind of class, declared with the record keyword, whose primary purpose is to hold data. You declare its components in the header, and the compiler generates a canonical constructor, private final fields, public accessor methods, equals(), hashCode(), and toString() automatically.
One line declares the components, and the constructor, accessors, equals/hashCode/toString are all generated.
public record Point(int x, int y) { }
// Usage
Point p = new Point(3, 4);
System.out.println(p.x()); // 3 (accessor, not getX())
System.out.println(p.y()); // 4
System.out.println(p); // Point[x=3, y=4]Why Records Exist
Before records, a simple immutable data holder required a lot of repetitive, error-prone boilerplate: a private final field per component, a constructor that assigns them, a getter per field, and correct equals()/hashCode()/toString() implementations. Records eliminate that ceremony for the common case where a class is nothing more than a fixed group of related values.
Records are about reducing boilerplate for data carriers, not about replacing every class. A class with real behavior, mutable state, or an identity distinct from its data is usually still a regular class.
Record Components
The names in the record header — int x, int y above — are called components. Each component becomes a private final field of the same name and type, plus a public accessor method with the same name (not prefixed with get).
public record Employee(String name, String department, double salary) { }
Employee e = new Employee("Ada", "Engineering", 95000.0);
e.name(); // "Ada"
e.department(); // "Engineering"
e.salary(); // 95000.0The Generated (Canonical) Constructor
The compiler generates a constructor whose parameter list matches the record components exactly, in the same order. This is called the canonical constructor. It simply assigns each parameter to its corresponding field.
public record Range(int low, int high) { }
// Equivalent generated constructor (conceptually):
// public Range(int low, int high) {
// this.low = low;
// this.high = high;
// }The Compact Constructor
When you need validation or normalization logic but don't want to repeat the full parameter list and field assignments, you can write a compact constructor. It has no parameter list — it reuses the record's declared components — and the field assignment happens implicitly at the end unless you throw first.
The compact constructor can validate or transform the incoming parameters; the final field assignment to this.low/this.high is inserted automatically after the body runs.
You can also write a full canonical constructor with an explicit parameter list and explicit this.field = ... assignments if you need more control, but the compact form is preferred when you're only validating or normalizing.
public record Range(int low, int high) {
public Range { // compact constructor: no parameter list
if (low > high) {
throw new IllegalArgumentException("low cannot exceed high");
}
// implicit: this.low = low; this.high = high;
}
}
new Range(1, 5); // OK
new Range(5, 1); // throws IllegalArgumentExceptionGenerated equals(), hashCode(), and toString()
Records generate equals() and hashCode() based on all components: two record instances are equal if they are the same record type and all corresponding components are equal. hashCode() is derived consistently from the same components. toString() produces a readable form like Point[x=3, y=4].
You can override any of equals(), hashCode(), or toString() explicitly in the record body if you need different semantics; the compiler only supplies the default when you don't.
record Point(int x, int y) { }
Point a = new Point(1, 2);
Point b = new Point(1, 2);
System.out.println(a.equals(b)); // true — same type, same components
System.out.println(a == b); // false — different objects
System.out.println(a.hashCode() == b.hashCode()); // true
System.out.println(a); // Point[x=1, y=2]Immutability of Components vs. Deep Immutability
Record fields are implicitly final, and there are no generated setters, so a record's own components cannot be reassigned after construction. This is shallow immutability: it guarantees the reference stored in each component cannot change. It does NOT guarantee the object that reference points to is itself immutable.
The items reference itself can't be reassigned, but nothing stops the referenced ArrayList from being mutated through the accessor. True deep immutability requires storing (or defensively copying into) an immutable collection, e.g. List.copyOf(list) inside a compact constructor.
Copying into an unmodifiable list inside the compact constructor gives the record true deep immutability for this component.
import java.util.ArrayList;
import java.util.List;
record Basket(List<String> items) { }
List<String> list = new ArrayList<>();
list.add("apple");
Basket basket = new Basket(list);
basket.items().add("banana"); // compiles and mutates the underlying list!
System.out.println(basket.items()); // [apple, banana]import java.util.List;
record Basket(List<String> items) {
public Basket {
items = List.copyOf(items); // defensive copy -> unmodifiable
}
}Records as Data Carriers
Records shine for modeling values whose identity is defined entirely by their data: coordinates, money amounts, DTOs (data transfer objects) returned from an API layer, keys in a map, or results returned from a method (like a pair of computed values).
record Result(boolean success, String message) { }
static Result validate(String input) {
if (input == null || input.isBlank()) {
return new Result(false, "Input cannot be blank");
}
return new Result(true, "OK");
}Records Can Have Additional Members
Beyond the generated members, a record body can declare static fields, static methods, instance methods, and additional (non-canonical) constructors that must delegate to the canonical constructor.
A record cannot declare additional instance fields beyond its components, cannot extend another class (records implicitly extend java.lang.Record), and is implicitly final — it cannot be subclassed. It can implement interfaces.
record Point(int x, int y) {
static final Point ORIGIN = new Point(0, 0);
Point() { // additional constructor
this(0, 0); // must delegate to the canonical constructor
}
double distanceFromOrigin() {
return Math.sqrt(x * x + y * y);
}
}Rules and Restrictions
Records come with a few fixed rules enforced by the compiler.
- A record is implicitly final: it cannot be extended.
- A record implicitly extends java.lang.Record (cannot extend any other class).
- A record can implement interfaces.
- Instance fields cannot be added beyond the declared components.
- Instance fields cannot be declared mutable (no extra non-final instance fields).
- The canonical/compact constructor must eventually assign every component.
- Non-canonical constructors must invoke another constructor via this(...).Records in switch and Pattern Matching
Later Java versions (16+ for records themselves; pattern matching for switch finalized in Java 21) let you destructure records directly in switch expressions and instanceof checks, which pairs naturally with records' transparent, fixed structure.
Java 21's record patterns let a case even destructure the components directly: case Circle(double radius) -> ....
sealed interface Shape permits Circle, Rectangle { }
record Circle(double radius) implements Shape { }
record Rectangle(double width, double height) implements Shape { }
static double area(Shape s) {
return switch (s) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.width() * r.height();
};
}Practical Usage
Common places records replace older patterns: value objects returned from service methods, immutable configuration snapshots, map/set keys (thanks to correct equals/hashCode), and lightweight tuples used to bundle multiple return values without writing a dedicated class.
import java.util.HashMap;
import java.util.Map;
record UserId(String value) { }
record UserProfile(String name, int age) { }
Map<UserId, UserProfile> users = new HashMap<>();
users.put(new UserId("u1"), new UserProfile("Grace", 34));
users.get(new UserId("u1")); // works correctly: equals/hashCode are structuralMental Model
Think of a record declaration as a contract: "this type is exactly these named, typed values, nothing more." The compiler holds up its end by generating the constructor and accessors that make that contract mechanical and consistent, freeing you to focus on validation logic (in a compact constructor) and any behavior methods you add.
Quick Reference
record Name(Type comp1, Type comp2, ...) { }
Generated automatically:
- private final fields for each component
- public accessor comp1(), comp2(), ... (no "get" prefix)
- canonical constructor Name(Type comp1, Type comp2, ...)
- equals(), hashCode(), toString() based on all components
You may add:
- compact constructor: public Name { ...validate... }
- static/instance methods, static fields, extra constructors (must delegate)
You may NOT:
- add extra instance fields
- extend another class
- be extended (implicitly final)Key Takeaways
A record declares components in its header; the compiler generates fields, a canonical constructor, accessors, equals(), hashCode(), and toString().
A compact constructor lets you validate or normalize incoming values without repeating the parameter list; field assignment happens implicitly afterward.
Records are shallowly immutable: components can't be reassigned, but a mutable object referenced by a component can still be mutated unless you defensively copy it.
Records are implicitly final, always extend java.lang.Record, and cannot declare extra instance fields.
Records are ideal for transparent data carriers — DTOs, value objects, map keys — not for classes with rich mutable state or identity beyond their data.
record Point(int x, int y) { public int x() { return this.x + 1; } // overriding accessor to "adjust" the value silently } record Point(int x, int y) { } // If you need transformed data, expose it via a differently named method instead: record PointOps(int x, int y) { int shiftedX() { return x + 1; } }
Silently overriding an accessor to return something other than the stored component violates the transparency contract records are meant to provide — callers expect x() to return the actual x component.
record Config(List<String> hosts) { } // caller mutates the "immutable" record's list config.hosts().add("new-host");
record Config(List<String> hosts) { public Config { hosts = List.copyOf(hosts); // defensive copy makes it truly unmodifiable } }
Assuming a record is fully immutable just because it's a record is a common mistake; mutable component types need a defensive copy in the compact constructor for real immutability.
record Employee(String name, double salary) extends Person { } // won't compile record Employee(String name, double salary) implements Identifiable { } // records cannot extend a class, but they can implement interfaces
Records cannot extend another class because they implicitly extend java.lang.Record; use interfaces to share behavior/contracts instead.
What members does the compiler automatically generate for record Point(int x, int y) {}?
Private final fields x and y, public accessors x() and y(), a canonical constructor Point(int, int), and equals(), hashCode(), and toString() based on both components.
What is the difference between a compact constructor and the canonical constructor?
The canonical constructor has the full parameter list and explicit field assignments. A compact constructor omits the parameter list (reusing the declared components), lets you validate/transform the parameters, and has the field assignments inserted implicitly at the end.
Why can a record still be effectively mutable even though its fields are final?
Final only prevents the component reference itself from being reassigned. If a component holds a mutable object (like a List or a mutable custom class), that object's internal state can still be changed through the accessor unless the record defensively copies it.
Can a record extend another class? Can it implement interfaces?
No, a record implicitly extends java.lang.Record and cannot extend anything else. Yes, it can implement one or more interfaces.
Why do records generate equals() and hashCode() based on components instead of identity?
Because records model pure data — two records representing the same values should be considered equal regardless of whether they are the same object instance, which also makes them safe and correct as map keys or set elements.
Interview: Why were records added to Java when classes could already do everything records do?
Records remove large amounts of repetitive, error-prone boilerplate (constructor, accessors, equals/hashCode/toString) for the extremely common case of a class that is just a fixed bundle of data, making that intent explicit and less bug-prone.
Interview: Is a record's immutability guaranteed by the language or just a convention?
The language guarantees the record's own components can't be reassigned (shallow immutability via implicit final fields and no generated setters). Deep immutability of referenced mutable objects is the programmer's responsibility, typically via defensive copying in a compact constructor.
Write a record Money with components long cents and String currency, including a compact constructor that rejects negative cents values.
public record Money(long cents, String currency) { public Money { if (cents < 0) { throw new IllegalArgumentException("cents cannot be negative"); } } }
The compact constructor validates cents before the implicit field assignment runs, throwing IllegalArgumentException for negative values while still assigning both components normally on success.
What does this print, and why do the two lines differ?
Prints false then true. == compares object reference identity, and p1/p2 are two distinct objects, so it's false. equals() is generated to compare all components structurally (both a and b match), so it's true.
Write a record Range that guarantees its min component is never greater than its max component, using a compact constructor.
public record Range(int min, int max) { public Range { if (min > max) { throw new IllegalArgumentException("min must be <= max"); } } }
The compact constructor checks the invariant before the fields are assigned; any attempt to build an invalid Range throws immediately at construction time.
What does this print, and what does it reveal about record immutability?
Prints [1, 2]. Because the record stores the same list reference passed in (no defensive copy), mutating the original list after construction is visible through the record's accessor — showing that records only guarantee the component reference is final, not that the referenced object is immutable.
record Pair(int a, int b) { }
Pair p1 = new Pair(1, 2);
Pair p2 = new Pair(1, 2);
System.out.println(p1 == p2);
System.out.println(p1.equals(p2));record Box(java.util.List<Integer> values) { }
var list = new java.util.ArrayList<Integer>();
list.add(1);
Box box = new Box(list);
list.add(2);
System.out.println(box.values());