Records (since Java 16) are a concise way to declare immutable data classes. The compiler automatically generates constructors, accessors, equals(), hashCode(), and toString().

Basic Record

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

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

A record implicitly:

  • Is final and cannot be extended
  • Has private final fields for each component
  • Provides a public constructor matching the components
  • Provides accessor methods named after components (no get prefix)

Compact Constructor

Validate or normalize values without repeating field assignments:

  public record Range(int start, int end) {
    public Range {
        if (start > end) {
            throw new IllegalArgumentException("start must be <= end");
        }
    }
}
  

The compact constructor assigns fields automatically after it runs.

Custom Methods and Static Members

Records can have additional methods, static fields, 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);
    }
}
  

Implementing Interfaces

  public record Person(String name, int age) implements Comparable<Person> {
    @Override
    public int compareTo(Person other) {
        return Integer.compare(this.age, other.age);
    }
}
  

Records vs Classes

Feature Record Class
Purpose Immutable data carrier General-purpose object
Mutability Immutable by design Mutable by default
Inheritance Cannot extend classes Can extend one class
Boilerplate Minimal Requires manual equals/hashCode/toString
Fields All final Any modifier

Limitations

  • Cannot extend other classes (already extends Record)
  • Cannot declare instance fields beyond the record components
  • Are implicitly final — cannot be extended
  • Cannot be abstract

When to Use Records

  • DTOs (Data Transfer Objects)
  • Value objects in domain models
  • Return types grouping multiple values
  • Keys in maps where equality matters
  public record UserDto(Long id, String email, String role) { }

Map<UserDto, List<Order>> ordersByUser = new HashMap<>();
  

Best Practices

  • Keep records focused on data — put complex behavior in separate service classes
  • Use compact constructors for validation
  • Prefer records over Lombok @Value for simple immutable data types
  • Use @Override when customizing equals() or toString() (rarely needed)