Java Records (Java 14+) — Immutable Data Carriers

4 minute read

Java records (preview in Java 14, standard in Java 16) are a concise way to create immutable data classes. They eliminate the boilerplate of writing equals(), hashCode(), toString(), getters, and constructors.


The Problem — Boilerplate

A simple data class in traditional Java:

public final class Point {
    private final double x;
    private final double y;

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

    public double getX() { return x; }
    public double getY() { return y; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Point p)) return false;
        return Double.compare(p.x, x) == 0 && Double.compare(p.y, y) == 0;
    }

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

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

~30 lines for two fields. 😩


The Solution — Record

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

One line. The compiler auto-generates:

  • private final fields x and y
  • A canonical constructor Point(double x, double y)
  • Accessor methods x() and y() (not getX())
  • equals() based on all fields
  • hashCode() based on all fields
  • toString() → Point[x=1.0, y=2.0]
Point p = new Point(3.0, 4.0);
System.out.println(p.x());      // 3.0
System.out.println(p.y());      // 4.0
System.out.println(p);          // Point[x=3.0, y=4.0]

Point p2 = new Point(3.0, 4.0);
System.out.println(p.equals(p2)); // true

Compact Constructors — Validation

Add validation without repeating the parameter list:

public record Temperature(double celsius) {
    public Temperature {   // compact constructor — no parens
        if (celsius < -273.15) {
            throw new IllegalArgumentException("Below absolute zero");
        }
    }
}

new Temperature(20);      // ✅
new Temperature(-300);    // ❌ IllegalArgumentException

The compact constructor runs before the fields are assigned. You can modify the parameters:

public record Email(String address) {
    public Email {
        address = address.toLowerCase().strip();
    }
}

Custom Methods

Records can have additional methods:

public record Rectangle(double width, double height) {

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

    public double perimeter() {
        return 2 * (width + height);
    }

    public boolean isSquare() {
        return width == height;
    }
}

Static Fields and Methods

public record Color(int r, int g, int b) {
    public static final Color RED   = new Color(255, 0, 0);
    public static final Color GREEN = new Color(0, 255, 0);
    public static final Color BLUE  = new Color(0, 0, 255);

    public String toHex() {
        return "#%02X%02X%02X".formatted(r, g, b);
    }
}

Records Implementing Interfaces

public interface Printable {
    String toPrettyString();
}

public record Person(String name, int age) implements Printable {
    @Override
    public String toPrettyString() {
        return name + " (age " + age + ")";
    }
}

Records can implement any number of interfaces but cannot extend a class (they implicitly extend java.lang.Record).


What Records Cannot Do

Feature Allowed?
Extend a class ❌
Be extended (subclassed) ❌ (implicitly final)
Mutable fields ❌ (all fields are final)
Instance fields beyond components ❌
Static fields ✅
Implement interfaces ✅
Custom methods ✅
Multiple constructors ✅ (must delegate to canonical)

Records as DTOs

Records are ideal for Data Transfer Objects:

public record UserDTO(long id, String name, String email) { }

public record ApiResponse<T>(int status, String message, T data) { }

Many frameworks (Jackson, Spring) now support records natively for JSON serialisation.


Records vs Classes vs Lombok

Feature Class Record Lombok @Data
Immutable by default ❌ ✅ ❌
Auto equals/hashCode ❌ ✅ ✅
Auto toString ❌ ✅ ✅
Mutable fields ✅ ❌ ✅
Inheritance ✅ ❌ ✅
No dependency ✅ ✅ ❌ (annotation processor)
Pattern matching ❌ ✅ (Java 21+) ❌

Use records when you need an immutable data carrier with value-based equality.


Records with Pattern Matching (Java 21+)

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

Object obj = new Point(3.0, 4.0);

if (obj instanceof Point(double x, double y)) {
    System.out.println("x=" + x + ", y=" + y);
}

// In switch
switch (shape) {
    case Point(var x, var y) -> System.out.println(x + ", " + y);
    default -> System.out.println("Unknown");
}

Sealed Interfaces with Records

public sealed interface Shape permits Circle, Rectangle {
    double area();
}

public record Circle(double radius) implements Shape {
    public double area() { return Math.PI * radius * radius; }
}

public record Rectangle(double w, double h) implements Shape {
    public double area() { return w * h; }
}

Local Records

Declare records inside a method for temporary data grouping:

public List<String> topScorers(List<Student> students) {
    record Score(String name, double avg) { }

    return students.stream()
        .map(s -> new Score(s.getName(), s.getAverage()))
        .filter(s -> s.avg() > 90)
        .map(Score::name)
        .toList();
}

Common Mistakes

Mistake Problem Fix
Expecting getX() Records use x() Call point.x() not point.getX()
Mutable field types List inside record is mutable Use List.copyOf() in compact constructor
Using record for mutable state Records are immutable Use a regular class
Extending a record Records are final Use interfaces instead

Defensive Copy for Mutable Components

public record Team(String name, List<String> members) {
    public Team {
        members = List.copyOf(members);  // defensive copy
    }
}

Key Takeaways

  1. Records eliminate boilerplate for immutable data classes.
  2. Auto-generated: constructor, accessors (x() not getX()), equals, hashCode, toString.
  3. Use compact constructors for validation and normalisation.
  4. Records can implement interfaces but cannot extend classes.
  5. Use defensive copies for mutable component types.
  6. Records are ideal for DTOs, value objects, and pattern matching.

Module Complete! 🎉

You’ve completed the Arrays, Strings & Utilities module. You now know how to:

  • Declare, manipulate, and search arrays
  • Work with Strings, StringBuilder, and regex
  • Use enums, Math, java.time, wrapper classes, Optional, and records

Next module: Exception Handling — mastering try-catch-finally, custom exceptions, and error handling best practices.

Updated: