Skip to content

Records — Compact Data Carriers, Canonical Constructors, and Components (Java 14+)

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Records. We cover key concepts, practical examples, and best practices to help you master this topic.

Java records are transparent data carriers that automatically generate constructors, accessors, equals, hashCode, and toString from component declarations. Records, introduced as a preview in Java 14 and standardized in Java 16, eliminate the boilerplate of writing POJOs — a single line defines the data shape and all common methods follow automatically.

What You'll Learn

  • Declaring records and understanding their components
  • Compact constructors for validation and normalization
  • Custom methods and static members in records
  • When records are appropriate and their limitations

Why It Matters

Before records, a simple data class required fields, constructor, getters, equals, hashCode, and toString — typically 50+ lines of boilerplate. Records reduce this to a single line, eliminating bugs from inconsistent equals/hashCode implementations.

Real-World Use

DTOs for REST APIs, event objects in messaging systems, value objects in domain-driven design, and tuple-like returns from methods. Records are now preferred over Lombok's @Data in many modern codebases.


Declaring a Record

public record Point(int x, int y) {}

This single line provides:

  • A canonical constructor: Point(int x, int y)
  • Accessor methods: x() and y() (not getX()/getY())
  • equals() — compares all components
  • hashCode() — derived from all components
  • toString() — formatted as Point[x=5, y=3]

Usage:

Point p = new Point(5, 3);
System.out.println(p.x());  // 5
System.out.println(p);      // Point[x=5, y=3]

The Canonical Constructor

The constructor generated by the compiler matches the components exactly:

// The compiler generates something like:
public Point(int x, int y) {
    this.x = x;
    this.y = y;
}

Compact Constructor

You can define a compact constructor to add validation or normalization without repeating the parameter assignments:

public record Point(int x, int y) {
    public Point {
        if (x < 0 || y < 0) {
            throw new IllegalArgumentException("Coordinates must be non-negative");
        }
        // x and y are automatically assigned to this.x and this.y
    }
}

The compact constructor body runs before the implicit field assignments. You can also normalize values:

public record Range(int min, int max) {
    public Range {
        if (min > max) {
            int temp = min;
            min = max;
            max = temp;
        }
    }
}

Non-Canonical Constructor

You can add additional constructors that delegate to the canonical one:

public record Point(int x, int y) {
    public Point() {
        this(0, 0); // delegates to canonical constructor
    }
}

Custom Methods

Records can have instance and static methods:

public record Rectangle(double width, double height) {
    public double area() {
        return width * height;
    }

    public static Rectangle square(double side) {
        return new Rectangle(side, side);
    }
}

Limitations

  • Records cannot extend other classes (they implicitly extend java.lang.Record)
  • Records are implicitly final — they cannot be abstract
  • Components are implicitly private final — there are no setters
  • Records cannot declare instance fields outside the component list
  • Reflection-based frameworks may need special handling (but records work with --add-opens)

Records and Serialization

Records implement Serializable if they declare they do. The serialized form is based on the component state. Custom readObject/writeObject methods are not allowed — the serialization mechanism uses the canonical constructor.

Records with Annotations

import javax.validation.constraints.NotNull;

public record User(
    @NotNull String name,
    @NotNull String email
) {}

Annotations on components are propagated to the constructor parameters, accessor methods, and fields.

Local Records

Records can be declared inside methods (local records), which is useful for intermediate data transformations:

public List<String> process(List<Order> orders) {
    record OrderSummary(String product, int totalQuantity) {}

    return orders.stream()
        .collect(Collectors.groupingBy(Order::product,
                 Collectors.summingInt(Order::quantity)))
        .entrySet().stream()
        .map(e -> new OrderSummary(e.getKey(), e.getValue()))
        .map(OrderSummary::toString)
        .toList();
}

Common Mistakes

  1. Trying to add setters. Records are immutable. You cannot modify a component after construction. To change a value, create a new record instance.
  2. Forgetting that accessors are x() not getX(). Records use the component name directly. Jackson can be configured to recognize them with @JsonAutoDetect.
  3. Using records with mutable components. A record containing a List or Date is not deeply immutable — the list can be modified externally. Always use immutable components or defensive copies.
  4. Expecting final fields to be writable via reflection. The JVM prevents modification of record components through reflection unless setAccessible is explicitly invoked.
  5. Using records with JPA entities. JPA entities require a no-arg constructor and mutable fields — records are unsuitable. Use records for DTOs and value objects instead.

Practice Questions

1. What methods does a record automatically generate?
Canonical constructor, component accessors, equals(), hashCode(), toString().

2. What is a compact constructor?
A constructor syntax that omits the parameter list — the parameters are the record components. Inside the compact constructor, you can add validation or normalization without repeating assignments.

3. Can a record have instance methods?
Yes. Records can have custom instance and static methods, but only the component list defines the state.

4. Why are records not suitable for JPA entities?
JPA entities require a no-arg constructor, mutable fields, and Lazy Loading proxies — all incompatible with records' immutability and finality.

5. What is the implicit superclass of all records?
java.lang.Record. It is an abstract class that records implicitly extend.

Challenge Question:
Create a BankTransaction record with components amount (double), description (String), and timestamp (LocalDateTime). Add a compact constructor that rejects negative amounts and empty descriptions. Add a static method credit(double amount, String description) that creates a Transaction with a positive amount and the current time. Also add an instance method formatted() that returns a formatted string.

FAQ

Can a record implement an interface?

Yes. Records can implement interfaces: public record Point(int x, int y) implements Printable { }. They must implement any abstract methods from the interface.

Can a record be used with Lombok?

Records already generate what Lombok provides. Using Lombok with records is redundant. Choose one or the other — records are the standard library solution.

Are records value types or reference types?

Records are reference types — they are stored on the heap. However, they behave like value types because of their value-based equality. Project Valhalla aims to introduce true value types in a future Java version.

Can I use records in Java 11?

No. Records require Java 14+ (preview) or Java 16+ (standard). If you are on Java 11, use Lombok's @Data or manually write the boilerplate.

How do I validate a record's state?

Use the compact constructor for validation. Throw IllegalArgumentException for invalid states. This guarantees that no record instance exists in an invalid state.

Mini Project

Write a program RecordDemo.java that:

  1. Defines a Person record with name, email, age
  2. Adds a compact constructor that validates email contains @ and age is non-negative
  3. Adds a method isAdult() that returns age >= 18
  4. Adds a static method fromCsv(String csvLine) that parses a comma-separated line into a Person
  5. Creates a List<Person> and demonstrates sorting by age using Comparator.comparing(Person::age)
  6. Shows that two records with the same components are equal
  7. Attempts to create a record with a null name and observes the NullPointerException (if using Objects.requireNonNull)

What's Next

Records give us transparent data carriers. But what if you need to restrict the set of possible subtypes? Lesson 20 introduces sealed classes (Java 17+) — classes and interfaces that control which other types can extend or implement them.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro