Skip to content
Happy Programming Guide
Start learning
Java

Java Records vs Classes (and vs Lombok)

Records are immutable data carriers with the boilerplate written for you. When to use one instead of a class, how they compare with Lombok, and the limits.

An open laptop on a wooden desk

A record is a class whose entire purpose is to hold data. You declare the fields once and Java writes the constructor, accessors, equals, hashCode and toString. Use a record when the type is a value with no hidden state; use a class when it has behaviour, mutability, or an identity of its own.

The same type, both ways#

JAVA
// The class version, roughly 40 lines in practice
public final class Point {
    private final int x;
    private final int y;

    public Point(int x, int y) { this.x = x; this.y = y; }
    public int x() { return x; }
    public int y() { return y; }

    @Override public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Point p)) return false;
        return x == p.x && y == p.y;
    }
    @Override public int hashCode() { return Objects.hash(x, y); }
    @Override public String toString() { return "Point[x=" + x + ", y=" + y + "]"; }
}
JAVA
// The record version
public record Point(int x, int y) {}

Those are equivalent. The record has a canonical constructor taking every component, an accessor per component named after it (no get prefix), and value-based equals, hashCode and toString.

What a record cannot do#

  • Change after construction. Every component is final. There are no setters.
  • Extend another class. Records implicitly extend java.lang.Record. They can implement interfaces.
  • Declare extra instance fields. The state is exactly the components. Static fields are allowed.
  • Be extended. Records are implicitly final.

Those are not limitations to work around; they are the definition. If you need any of them, you want a class.

Record or class#

The type… Use
is a bundle of values, compared by content record
is a DTO, API response, config, event, message record
is a map key or set element record
has fields that change over time class
has identity: two with the same data are different things class
is a JPA entity class (JPA needs a no-arg constructor and mutability)
has real behaviour beyond derived values usually class

The one-line test: if two instances with the same field values are interchangeable, it is a record.

Validation and derived values#

A compact constructor runs before the fields are assigned and is where validation and normalisation go:

JAVA
public record Range(int low, int high) {
    public Range {                                   // no parameter list
        if (low > high) throw new IllegalArgumentException("low > high");
    }

    public int length() { return high - low; }       // methods are fine

    public static Range of(int a, int b) {           // static factories are fine
        return new Range(Math.min(a, b), Math.max(a, b));
    }
}

You can also reassign a parameter inside the compact constructor to normalise it — trimming a string, say — and the normalised value is what gets stored.

Records vs Lombok#

Lombok’s @Data and @Value generate similar boilerplate by rewriting the class at compile time. The comparison:

Record Lombok @Value Lombok @Data
Part of the language yes no, annotation processor no
Immutable always yes no, generates setters
Accessor style x() getX() getX()
Can extend a class no yes yes
Extra fields no yes yes
Breaks on new JDK releases never occasionally, until Lombok updates same
Works with pattern matching yes, deconstruction no no

For new immutable data types, records win: no dependency, no build-tool configuration, no lag when a new JDK ships. Lombok remains useful for mutable beans, for builders on large classes, and for codebases that already use it everywhere. Mixing the two in one project is fine; use the record where you can and Lombok where you must.

Records in pattern matching#

Records can be deconstructed, which is where they stop being merely convenient and start changing how you write code:

JAVA
sealed interface Shape permits Circle, Square {}
record Circle(double radius) implements Shape {}
record Square(double side) implements Shape {}

double area(Shape s) {
    return switch (s) {
        case Circle(double r) -> Math.PI * r * r;
        case Square(double side) -> side * side;
    };
}

The switch is exhaustive because the interface is sealed, and each case pulls the components straight out. Nested patterns work too: case Line(Point(var x1, var y1), Point(var x2, var y2)).

Records as keys#

JAVA
record Key(String region, int year) {}

Map<Key, Report> reports = new HashMap<>();
reports.put(new Key("EU", 2026), report);
reports.get(new Key("EU", 2026));      // found - equals and hashCode are by value

A class would need hand-written equals and hashCode for this to work. A record gets them right automatically, which removes an entire category of subtle bugs.

Questions people ask#

Can a record have methods?

Yes: instance methods, static methods, static fields, and nested types. Only extra instance fields and setters are ruled out.

Can I add a second constructor?

Yes, as long as it delegates to the canonical one with this(...).

Are records slower?

No. They compile to ordinary classes. The generated equals and hashCode are at least as efficient as typical hand-written ones.

Which Java version introduced records?

Final in Java 16. Any supported LTS — 17, 21, 25 — has them.

Where to go next#

What's new in Java 25, the current long-term releaseRead next

Keep reading

Keep going — pick your next guide

The fastest way to improve is to read one guide, then build the thing it describes. Start with the basics, or jump straight to a project.

Ask a question or share what worked

Your email address will not be published. Required fields are marked *