Java Records

Replace boilerplate data classes with records, which generate constructors, accessors, equals, hashCode and toString for you.

Lesson 93 of 127Intermediate17 min read

Many classes exist simply to hold data: a point with an x and a y, a product with a name and a price, a search result with a title and a link. Written as a normal class, each one needs private fields, a constructor, a getter for every field, and usually equals, hashCode and toString too. That can be fifty lines of code that says almost nothing new.

A record, added in Java 16, does all of that in a single line. You list the components, and Java generates the rest. Records are also immutable: once created, their values can't change, which makes them safe to share, use as map keys and pass between parts of a program.

In this lesson you'll declare records, see what Java generates, validate values in a compact constructor, add your own methods, make changed copies, and learn when a regular class is still the better choice.

Declaring a record

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

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

From that one line, Java creates:

  • A private final field for each component.
  • A constructor taking every component in order.
  • An accessor method for each component, named like the component: x(), not getX().
  • equals() and hashCode() that compare all the components.
  • A readable toString().

Immutability and equality

Record fields are final, and there are no setters, so a record can't be changed after it's created. To "change" one, create a new record with the new value.

Two records are equal when all their components are equal, even if they're different objects:

Java
new Point(1, 2).equals(new Point(1, 2))   // true

That makes records ideal as HashMap keys and HashSet items, and in tests, where you compare expected and actual values. One caution: if a component is a mutable object, such as a List, the list itself can still be changed. Store List.copyOf(list) to make it truly fixed.

Compact constructors and methods

To check values when a record is created, add a compact constructor: the record's name with no brackets. The assignments to the fields happen automatically after it runs:

Java
record Product(String name, double price) {
    Product {
        if (price < 0) throw new IllegalArgumentException("price < 0");
        name = name.trim();
    }
}

You can also add ordinary methods, static methods and static factory methods inside the braces. You can't add extra instance fields: everything a record holds must be listed in its header.

Records or classes?

Use a record whenUse a class when
It mainly carries dataIt has changing state, like a bank balance
Values shouldn't change after creationIt needs setters or extra hidden fields
Equality should mean "same values"Identity matters: two objects are different even with the same values
Examples: Point, Money, SearchResult, DTOsExamples: BankAccount, GameCharacter, Connection

Records can implement interfaces, but they can't extend another class, and nothing can extend a record.

Records and pattern matching

Since Java 21, record patterns can take a record apart while checking its type. The components are bound to variables in one step:

Java
Object shape = new Point(3, 4);
if (shape instanceof Point(int x, int y)) {
    System.out.println(x + y);    // 7
}

Record patterns also work in switch, which pairs well with sealed interfaces: each case names a record type and pulls out exactly the components it needs. This style is especially readable for data that comes in a few fixed shapes, such as events, commands or API responses. The modern Java features lesson shows more examples.

Java Records Syntax

Record syntax

record Name(Type a, Type b) {
    Name {                       // compact constructor
        // validate a and b
    }
    Type method() { ... }
    static Name of(...) { ... }
}
Name n = new Name(a, b);
n.a();  n.b();

Java Records Summary

  • record Name(Type a, Type b) {} creates an immutable data class in one line.
  • Java generates the constructor, accessors like a(), equals, hashCode and toString.
  • Records with the same component values are equal, so they work well as map keys.
  • A compact constructor validates or normalises values; you can add methods but not extra fields.
  • Use records for data carriers and classes for objects with changing state.

Java Records Examples

Each example shows the code, the output it produces and what each part does.

Example 1

A record compared with a class

Java
import java.util.HashSet;
import java.util.Set;

public class Main {
    record Point(int x, int y) {
        double distanceFromOrigin() {
            return Math.sqrt(x * x + y * y);
        }
    }

    public static void main(String[] args) {
        Point a = new Point(3, 4);
        Point b = new Point(3, 4);

        System.out.println(a);
        System.out.println("x is " + a.x());
        System.out.println("Same object? " + (a == b));
        System.out.println("Equal? " + a.equals(b));
        System.out.println("Distance: " + a.distanceFromOrigin());

        Set<Point> visited = new HashSet<>();
        visited.add(a);
        visited.add(b);
        System.out.println("Unique points: " + visited.size());
    }
}
Output
Point[x=3, y=4]
x is 3
Same object? false
Equal? true
Distance: 5.0
Unique points: 1
Line-by-line explanation
  1. Line 5One line replaces fields, constructor, getters, equals, hashCode and toString.
  2. Lines 6 to 8Records can have their own methods, using the components directly.
  3. Lines 17 to 18Two separate objects, but equal because their values match.
  4. Lines 21 to 23The set treats them as the same point.
Example 2

Validating with a compact constructor

Java
public class Main {
    record Money(long pence, String currency) {
        Money {
            if (pence < 0) {
                throw new IllegalArgumentException("Amount can't be negative");
            }
            currency = currency.toUpperCase();
        }

        Money plus(Money other) {
            return new Money(pence + other.pence, currency);
        }

        @Override
        public String toString() {
            return String.format("%d.%02d %s", pence / 100, pence % 100, currency);
        }
    }

    public static void main(String[] args) {
        Money price = new Money(1250, "gbp");
        Money total = price.plus(new Money(399, "GBP"));
        System.out.println(total);

        try {
            new Money(-5, "GBP");
        } catch (IllegalArgumentException e) {
            System.out.println("Rejected: " + e.getMessage());
        }
    }
}
Output
16.49 GBP
Rejected: Amount can't be negative
Line-by-line explanation
  1. Lines 3 to 8The compact constructor checks the amount and tidies the currency before the fields are set.
  2. Lines 10 to 12Instead of changing the record, plus returns a new one.
  3. Lines 14 to 17You can replace the generated toString with your own.
  4. Line 21"gbp" is stored as "GBP" thanks to the constructor.
Example 3

Copies with a change, and records as keys

Java
import java.util.HashMap;
import java.util.Map;

public class Main {
    record Seat(char row, int number) {}

    record Booking(String name, Seat seat, boolean paid) {
        Booking markPaid() {
            return new Booking(name, seat, true);
        }
    }

    public static void main(String[] args) {
        Booking first = new Booking("Ana", new Seat('C', 12), false);
        Booking paid = first.markPaid();
        System.out.println(first);
        System.out.println(paid);

        Map<Seat, String> taken = new HashMap<>();
        taken.put(paid.seat(), paid.name());
        System.out.println("C12 taken by " + taken.get(new Seat('C', 12)));
    }
}
Output
Booking[name=Ana, seat=Seat[row=C, number=12], paid=false]
Booking[name=Ana, seat=Seat[row=C, number=12], paid=true]
C12 taken by Ana
Line-by-line explanation
  1. Lines 8 to 10A "with" style method returns a copy with one value changed. The original is untouched.
  2. Lines 16 to 17Nested records print neatly too.
  3. Line 21A brand new Seat('C', 12) finds the entry, because records with equal values have equal hash codes.

Common Java Records Mistakes

Calling getX() on a record

Don't
Point p = new Point(1, 2);
int x = p.getX();          // no such method
Do
int x = p.x();

Trying to change a record

Don't
record Player(String name, int score) {}
player.score = 10;         // score is final
Do
player = new Player(player.name(), 10);

Adding extra instance fields

Don't
record User(String name) {
    private int loginCount;     // not allowed in a record
}
Do
class User {
    private final String name;
    private int loginCount;     // changing state belongs in a class
    // ...
}

Java Records in Real Projects

Records in practice

Modern Java code uses records for the data that flows through an application: API request and response bodies, database query results, configuration values, events and messages. Frameworks such as Spring and libraries like Jackson read and write records as JSON directly. Records also pair with streams and sorting, because their accessors work neatly as method references like Player::score.

Java Records FAQ

Can a record have setters?

No. Record fields are final, so they can't change after construction. Instead of a setter, add a method that returns a new record with one value changed, for example Product withPrice(double newPrice) { return new Product(name, newPrice); }.

Can a record have static fields and methods?

Yes. Static fields, static methods and static factory methods such as Point.origin() are all allowed. Only extra instance fields are forbidden, because all instance data must be listed in the record header.

Can records be used as JPA entities?

Not as entities. JPA providers such as Hibernate need entities they can create empty and then change, which conflicts with immutable records. Records work very well for data transfer objects, query results and API request and response bodies.

Do records work with JSON libraries?

Yes. Jackson has supported records since version 2.12, and Gson and other modern libraries handle them too. Each component becomes a JSON property with the same name, and the record's constructor is used when reading JSON back.

Which Java version do I need for records?

Records became a standard feature in Java 16, so any current long-term support release, such as Java 17, 21 or 25, supports them. Record patterns, which take records apart in instanceof and switch, need Java 21 or later.

Java Records Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Complete the declaration of a record with a title and a year.

 Film(String title, int year) {}
Multiple choiceeasy

How do you read the title of Film f?

Predict the outputmedium

What does this print?

record Pair(int a, int b) {}
System.out.println(new Pair(1, 2).equals(new Pair(1, 2)));
Multiple choicemedium

Where do you validate a record's values?

Multiple choicehard

Which is the best choice for a bank account whose balance changes?

Java Records Quiz

5 questions to check what you learned.

  1. Which version of Java made records a standard feature?

    • Java 8
    • Java 11
    • Java 16
    • Java 21
  2. Which methods does a record generate?

    • Only a constructor
    • A constructor, accessors, equals, hashCode and toString
    • Setters and getters
    • main
  3. Can a record extend another class?

    • Yes
    • Only abstract classes
    • Only other records
    • No, but it can implement interfaces
  4. How do you "change" a value in a record?

    • Use a setter
    • Create a new record with the new value
    • Assign to the field
    • Call update()
  5. Why are records good HashMap keys?

    • Equal values give equal hash codes, and the values can't change after insertion
    • They're faster to create
    • Records are sorted
    • HashMap requires records