Records

Su Suriya Ravichandran Updated 14 Nov 2026
8 min read ·Lesson 63 of 133

A record is a special kind of class designed to hold immutable data. You declare the data it carries, its components, and the compiler generates the private final fields, constructor, accessor methods, equals, hashCode, and toString automatically. Records became a standard feature in Java 16. They replace a large amount of repetitive code for the many classes whose job is simply to carry values: API responses, database rows, coordinates, money amounts, configuration entries, and method results.

The Problem Records Solve

A simple immutable class holding two values requires a surprising amount of code:

import java.util.Objects;

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 other)) return false;
        return x == other.x && y == other.y;
    }

    @Override
    public int hashCode() { return Objects.hash(x, y); }

    @Override
    public String toString() { return "Point[x=" + x + ", y=" + y + "]"; }
}

The same type as a record:

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

Both versions behave essentially the same. The record is shorter, but more importantly, it cannot drift out of sync: when a component is added, equals, hashCode, toString, and the constructor are all updated automatically.

What the Compiler Generates

For record Point(int x, int y):

Generated memberDetails
private final int x; private final int y;One field per component
public Point(int x, int y)The canonical constructor, assigning every field
public int x(), public int y()Accessors named after the components (no get prefix)
equals(Object)True when the other object is a Point with equal components
hashCode()Computed from all components
toString()Point[x=3, y=4]

The record class is implicitly final and extends java.lang.Record.

Point p = new Point(3, 4);
Point q = new Point(3, 4);

System.out.println(p);              // Point[x=3, y=4]
System.out.println(p.x());          // 3
System.out.println(p.equals(q));    // true: compared by value
System.out.println(p == q);         // false: two different objects
System.out.println(p.hashCode() == q.hashCode());   // true

Value-based equals and hashCode mean records work correctly as HashMap keys and HashSet elements without any extra code:

Set<Point> visited = new HashSet<>();
visited.add(new Point(1, 2));
System.out.println(visited.contains(new Point(1, 2)));   // true

Immutability

Record fields are private and final, and there are no setters. Once created, a record's components cannot be changed: code outside the record cannot even see the fields, and code inside it cannot reassign them after construction.

Point p = new Point(3, 4);
p.x = 10;          // compile error: x has private access in Point

To "change" a record, create a new one, often with a helper method that returns a modified copy:

public record Point(int x, int y) {
    public Point withX(int newX) {
        return new Point(newX, y);
    }

    public Point translate(int dx, int dy) {
        return new Point(x + dx, y + dy);
    }
}

Point a = new Point(1, 1);
Point b = a.translate(2, 3);     // Point[x=3, y=4]; a is unchanged

Shallow immutability

A record is only as immutable as its components. A mutable component, such as a List, can still be modified:

record Team(String name, List<String> members) { }

List<String> people = new ArrayList<>(List.of("Ada"));
Team t = new Team("Core", people);
people.add("Mallory");                    // modifies the list inside the record
t.members().add("Eve");                   // so does this
System.out.println(t.members());          // [Ada, Mallory, Eve]

The fix is a defensive copy in a compact constructor, described next:

record Team(String name, List<String> members) {
    Team {
        members = List.copyOf(members);   // unmodifiable copy, independent of the caller's list
    }
}

Compact Constructors: Validation and Normalization

A compact constructor declares no parameter list. It runs before the fields are assigned, can validate the arguments, and can reassign the parameters to normalize them. The compiler then assigns the (possibly modified) parameters to the fields:

public record Email(String value) {
    public Email {
        if (value == null || !value.matches("[^@\\s]+@[^@\\s]+\\.[^@\\s]+")) {
            throw new IllegalArgumentException("Invalid email: " + value);
        }
        value = value.strip().toLowerCase();
    }
}
Email e = new Email("Ada@Example.COM");
System.out.println(e.value());            // ada@example.com

new Email("not-an-email");                // IllegalArgumentException

Every Email object that exists in the program is valid and normalized, because the constructor is the only way to create one.

Inside a compact constructor, do not assign this.value yourself; assignment to the fields happens automatically at the end. You can instead write the full canonical constructor explicitly when you need complete control:

public record Range(int min, int max) {
    public Range(int min, int max) {
        if (min > max) throw new IllegalArgumentException(min + " > " + max);
        this.min = min;
        this.max = max;
    }
}

Additional constructors

A record may declare other constructors, but each must delegate to the canonical one with this(...):

public record Money(long paise, String currency) {
    public Money {
        if (paise < 0) throw new IllegalArgumentException("negative amount");
        currency = currency.toUpperCase();
    }

    public Money(double rupees) {
        this(Math.round(rupees * 100), "INR");
    }
}

System.out.println(new Money(149.99));    // Money[paise=14999, currency=INR]

Adding Methods, Static Members, and Interfaces

Records can contain:

  • Instance methods
  • Static fields and static methods (including factory methods)
  • Nested types
  • Implementations of interfaces
public record Temperature(double celsius) implements Comparable<Temperature> {
    public static final Temperature FREEZING = new Temperature(0);

    public static Temperature ofFahrenheit(double f) {
        return new Temperature((f - 32) * 5 / 9);
    }

    public double fahrenheit() {
        return celsius * 9 / 5 + 32;
    }

    @Override
    public int compareTo(Temperature other) {
        return Double.compare(celsius, other.celsius);
    }
}
Temperature body = Temperature.ofFahrenheit(98.6);
System.out.printf("%.1f°C%n", body.celsius());          // 37.0°C
System.out.println(body.compareTo(Temperature.FREEZING) > 0);   // true

You can also override generated members, such as toString or an accessor, when needed:

public record Password(String hash) {
    @Override
    public String toString() {
        return "Password[hash=***]";         // never print secrets in logs
    }
}

What Records Cannot Do

RestrictionReason
Cannot extend another classThey already extend java.lang.Record
Cannot be extended (implicitly final)A subclass could add state and break value semantics
Cannot declare additional instance fieldsAll state must be in the components, so equals and hashCode stay complete
Fields are always finalRecords are data carriers with fixed values
Not suited to JPA entitiesEntities need mutable state and a no-argument constructor

Static fields are allowed, since they are not part of an individual record's state.

Records in Practice

Returning multiple values

record MinMax(int min, int max) { }

static MinMax minMax(int[] values) {
    int min = Integer.MAX_VALUE, max = Integer.MIN_VALUE;
    for (int v : values) {
        min = Math.min(min, v);
        max = Math.max(max, v);
    }
    return new MinMax(min, max);
}

MinMax r = minMax(new int[]{7, 2, 9, 4});
System.out.println(r.min() + "–" + r.max());   // 2–9

Local records

Records can be declared inside a method when a small intermediate type is needed only there:

static void topScorer(List<String> names, List<Integer> scores) {
    record Entry(String name, int score) { }

    List<Entry> entries = new ArrayList<>();
    for (int i = 0; i < names.size(); i++) {
        entries.add(new Entry(names.get(i), scores.get(i)));
    }
    entries.sort(Comparator.comparingInt(Entry::score).reversed());
    System.out.println(entries.get(0));
}

topScorer(List.of("Ada", "Linus"), List.of(88, 93));   // Entry[name=Linus, score=93]

Record patterns (Java 21+)

Records can be deconstructed in instanceof and switch:

sealed interface Shape permits Circle, Rect { }
record Circle(double r) implements Shape { }
record Rect(double w, double h) implements Shape { }

static double area(Shape s) {
    return switch (s) {
        case Circle(double r) -> Math.PI * r * r;
        case Rect(double w, double h) -> w * h;
    };
}

Combined with sealed interfaces, records let the compiler check that every variant is handled, a style often called algebraic data types.

Data transfer and JSON

Records are a natural fit for data moving between layers: request and response bodies, query results, messages. Current versions of popular JSON libraries such as Jackson and Gson can serialize and deserialize records using their canonical constructor and accessors.

Records Versus Regular Classes

Use a record whenUse a regular class when
The type is primarily dataThe type has significant behavior and mutable state
Equality should be based on valuesIdentity matters (two accounts with equal balances are still different accounts)
Immutability is appropriateFields must change over the object's life
All state is in a fixed list of componentsYou need inheritance or hidden internal state

Practical Example: Course Catalog Query

import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class Catalog {
    enum Level { BEGINNER, INTERMEDIATE, ADVANCED }

    record Course(String code, String title, Level level, int hours, double priceInr) {
        Course {
            if (hours <= 0) throw new IllegalArgumentException("hours must be positive");
            if (priceInr < 0) throw new IllegalArgumentException("price cannot be negative");
            code = code.toUpperCase();
        }

        boolean isFree() { return priceInr == 0; }

        double pricePerHour() { return priceInr / hours; }
    }

    record LevelSummary(Level level, long count, int totalHours) { }

    public static void main(String[] args) {
        List<Course> courses = List.of(
            new Course("java101", "Java Basics", Level.BEGINNER, 20, 0),
            new Course("dj201", "Django Web Apps", Level.INTERMEDIATE, 30, 2999),
            new Course("dk210", "Docker Essentials", Level.INTERMEDIATE, 12, 1499),
            new Course("cpp301", "Modern C++", Level.ADVANCED, 40, 3999)
        );

        Map<Level, List<Course>> byLevel = courses.stream()
                .collect(Collectors.groupingBy(Course::level));

        for (Level lvl : Level.values()) {
            List<Course> list = byLevel.getOrDefault(lvl, List.of());
            int hours = list.stream().mapToInt(Course::hours).sum();
            System.out.println(new LevelSummary(lvl, list.size(), hours));
        }

        Course bestValue = courses.stream()
                .filter(c -> !c.isFree())
                .min(Comparator.comparingDouble(Course::pricePerHour))
                .orElseThrow();
        System.out.printf("Best value: %s (%.2f per hour)%n", bestValue.title(), bestValue.pricePerHour());
    }
}

Output:

LevelSummary[level=BEGINNER, count=1, totalHours=20]
LevelSummary[level=INTERMEDIATE, count=2, totalHours=42]
LevelSummary[level=ADVANCED, count=1, totalHours=40]
Best value: Django Web Apps (99.97 per hour)

Course is a record with five components, a compact constructor that validates hours and price and normalizes the code, and two derived methods. LevelSummary is a second record used purely to carry a computed result, and its generated toString provides readable output for free. Among the paid courses, Django costs 2999 / 30 ≈ 99.97 per hour, just below Modern C++ at 3999 / 40 ≈ 99.98, so it is reported as the best value. Method references such as Course::level and Course::hours work because the accessors are ordinary methods. The streams used here are covered in their own section; the point is that records make the data model of a small program compact, correct, and immutable with almost no code.

0 Comments

Reviewed before they appear

No comments yet.

Java
Java Introduction Java Features Java Syntax Java Comments Java Statements Java Output Java Variables Java Data Types Java Type Casting Java Operators Java Strings Java Numbers Java Math Java Booleans Java User Input Java Conditions Java If Else Java Switch Java While Loop Java For Loop Java Break Java Continue Java Arrays Java Methods Java Parameters Java Return Values Java Scope Java Recursion Install Java Install JDK Java IDEs Java JDK Java JRE Java JVM Java Compiler Java File Compile Java Run Java JShell Basics Java Packages Java OOP Java Classes Java Objects Class Methods Constructors Constructor Overload this Keyword static Keyword final Keyword Encapsulation Inheritance Polymorphism Abstraction Access Modifiers super Keyword Method Overload Method Override Instanceof Operator Nested Classes Inner Classes Anonymous Classes Enums Records Array Basics Array Length Loop Arrays Multidimensional Arrays Array Copy Array Sorting Array Search ArrayList LinkedList HashSet TreeSet HashMap TreeMap Queue Interface Deque Interface Stack Class Iterator List Interface Set Interface Map Interface Collections Class Comparable Comparator Generics Basics Generic Classes Generic Methods Wildcards String Basics String Length String Methods String Concatenation String Compare String Search String Replace String Split String Format StringBuilder StringBuffer Text Blocks Regular Expressions Number Types Integer Class Float Class Double Class BigInteger BigDecimal Math Class Random Numbers Number Format Date Basics LocalDate LocalTime LocalDateTime ZonedDateTime Date Formatting Date Parsing Period Class Duration Class Java Instant Class Java Exception Basics Java try...catch Java Multiple Catch Blocks Java finally Block Java throw Keyword Java throws Keyword Java Checked Exceptions Java Unchecked Exceptions Java Custom Exceptions Java try-with-resources Java Exception Chaining
Ask about this post
AI Ask about this post

Ask questions about Records and get answers drawn from it.

Signed-in readers only.