Java Records Cheat Sheet
Covers Java record syntax, compact constructors, added methods, record pattern matching, and key immutability rules for data carriers.
Declaring a Record
A record is an immutable data carrier with auto-generated boilerplate.
// Implicitly final, implements equals(), hashCode(), toString(), and accessorspublic record Point(int x, int y) {}Point p = new Point(3, 4);System.out.println(p.x()); // 3 (accessor, not getX())System.out.println(p); // Point[x=3, y=4]System.out.println(p.equals(new Point(3, 4))); // true
Compact Constructor
Validate or normalize fields without repeating the parameter list.
public record Range(int min, int max) { // Compact constructor: no parameter list, fields are implicitly assigned after public Range { if (min > max) { throw new IllegalArgumentException("min must be <= max"); } }}new Range(5, 1); // throws IllegalArgumentException
Adding Methods & Static Members
Records can declare extra instance methods, static fields, and static factories.
public record Point(int x, int y) { static final Point ORIGIN = new Point(0, 0); double distanceToOrigin() { return Math.sqrt(x * x + y * y); } static Point of(int x, int y) { return new Point(x, y); }}
Record Patterns (Java 21+)
Deconstruct records in switch and instanceof for concise pattern matching.
Object obj = new Point(1, 2);if (obj instanceof Point(int x, int y)) { System.out.println(x + y);}String label = switch (obj) { case Point(int x, int y) when x == 0 && y == 0 -> "origin"; case Point(int x, int y) -> "point at " + x + "," + y; default -> "unknown";};
Key Facts
Rules and restrictions that define record behavior.
- Immutability- All fields are implicitly private and final; no setters are generated.
- Canonical Constructor- Auto-generated constructor matching the component list unless a compact or explicit one is defined.
- Accessors- Generated accessor methods are named after the field, not getX() - e.g. x() not getX().
- No Extra Instance Fields- Records cannot declare additional non-static instance fields beyond their components.
- Implicit final- A record class is implicitly final and cannot be extended.
- Can Implement Interfaces- Records can implement interfaces but cannot extend another class (they implicitly extend java.lang.Record).
Algebraic Data Types with Sealed + Records
Combine sealed interfaces and records to model closed sets of variants with exhaustive, compiler-checked handling.
sealed interface PaymentEvent permits Authorized, Captured, Refunded {}record Authorized(String txId, BigDecimal amount) implements PaymentEvent {}record Captured(String txId, Instant capturedAt) implements PaymentEvent {}record Refunded(String txId, BigDecimal amount, String reason) implements PaymentEvent {}String describe(PaymentEvent event) { return switch (event) { case Authorized(String id, BigDecimal amt) -> id + " authorized for " + amt; case Captured(String id, Instant at) -> id + " captured at " + at; case Refunded(String id, BigDecimal amt, String reason) -> id + " refunded " + amt + ": " + reason; }; // exhaustive - compiler errors if a permitted type is missed}
Nested Deconstruction with Guards
Record patterns can nest arbitrarily deep and combine with when-clauses for precise matching.
record Address(String city, String zip) {}record Customer(String name, Address address) {}record Order(Customer customer, BigDecimal total) {}String classify(Object obj) { return switch (obj) { case Order(Customer(String name, Address(String city, var zip)), var total) when total.compareTo(BigDecimal.valueOf(1000)) > 0 -> name + " in " + city + " placed a high-value order"; case Order(Customer(var name, var address), var total) -> name + " placed a standard order"; default -> "not an order"; };}
Generic Records with Validation
Type parameters and a compact constructor combine to build a reusable, self-validating result wrapper.
public record Result<T>(T value, List<String> errors) { public Result { errors = List.copyOf(errors); // defensive copy - prevents external mutation if (value == null && errors.isEmpty()) { throw new IllegalStateException("Result must have a value or at least one error"); } } public static <T> Result<T> ok(T value) { return new Result<>(value, List.of()); } public static <T> Result<T> failure(String error) { return new Result<>(null, List.of(error)); } public boolean isOk() { return errors.isEmpty(); }}
JSON Deserialization with Jackson
Records deserialize out of the box with jackson-databind 2.12+, but constructor-parameter names must be preserved or annotated explicitly.
// Works automatically if compiled with -parameters (Spring Boot does this by default)public record UserDto(String username, String email) {}ObjectMapper mapper = new ObjectMapper();UserDto user = mapper.readValue(""" {"username":"alice","email":"[email protected]"} """, UserDto.class);// Without -parameters, annotate the canonical constructor explicitly:public record StrictUserDto( @JsonProperty("username") String username, @JsonProperty("email") String email) { @JsonCreator public StrictUserDto { } // compact constructor still triggers @JsonCreator binding}
Records vs Other Immutable-Class Approaches
Trade-offs versus Lombok, classic POJOs, and manually-written immutable classes.
- Records vs Lombok @Value- Records are a language feature with no build-time annotation processor required; @Value needs Lombok on the classpath and IDE plugin support.
- Records vs classic POJO- A POJO with getters/setters needs 5-10x the boilerplate and is mutable by default; a record is immutable and concise by construction.
- Records vs manual immutable class- Hand-written immutable classes still need equals()/hashCode()/toString() maintained by hand; records generate and keep these in sync automatically.
- Inheritance- Records cannot extend a class (only implement interfaces); Lombok-annotated classes and manual classes can still participate in class hierarchies.
- Framework compatibility- JPA entities, Hibernate proxies, and CGLIB-based mocking frameworks generally require non-final mutable classes, so records are unsuitable there.
- Serialization footprint- Records serialize with java.io.Serializable support out of the box when declared, same as any other class, but component order affects the default serial form.
Use records for DTOs and immutable value objects, but keep JPA/Hibernate entities as regular classes - records' lack of a mutable state and final fields conflicts with how JPA proxies and lazy loading work.