Contents
- Declaration and Auto-generated Members
- Compact Constructors
- Adding Custom Methods
- Implementing Interfaces
- Records vs Regular Classes
A record is declared with the record keyword followed by a list of components in parentheses. Each component becomes a private final field plus a public accessor method with the same name (not getX(), just x()).
// Declaration — replaces ~50 lines of boilerplate
record Point(int x, int y) {}
// What the compiler auto-generates:
// 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; }
// public boolean equals(Object o) { ... }
// public int hashCode() { ... }
// public String toString() { return "Point[x=1, y=2]"; }
Point p = new Point(3, 7);
System.out.println(p.x()); // 3
System.out.println(p.y()); // 7
System.out.println(p); // Point[x=3, y=7]
// equals() compares all components by value
Point p2 = new Point(3, 7);
System.out.println(p.equals(p2)); // true
// Records work perfectly in collections and as map keys
Set<Point> set = new HashSet<>();
set.add(new Point(1, 2));
set.add(new Point(1, 2)); // duplicate — not added
System.out.println(set.size()); // 1
Record fields are always private final and there are no setters. Records are inherently immutable — this makes them thread-safe and safe to use as map keys.
A compact constructor lets you add validation or normalization logic without repeating the parameter assignments — the compiler inserts the assignments automatically at the end.
record Range(int min, int max) {
// Compact constructor — no parameter list, no field assignments needed
Range {
if (min > max)
throw new IllegalArgumentException(
"min (%d) must be <= max (%d)".formatted(min, max));
// Compiler inserts: this.min = min; this.max = max;
}
}
record Person(String name, int age) {
Person {
Objects.requireNonNull(name, "name must not be null");
name = name.strip(); // normalize before assignment
if (age < 0) throw new IllegalArgumentException("age must be >= 0");
}
}
Person p = new Person(" Alice ", 30);
System.out.println(p.name()); // "Alice" — stripped
You can also override the canonical constructor explicitly, but compact constructors are cleaner for validation:
record Coordinate(double lat, double lon) {
// Explicit canonical constructor (less common — prefer compact)
Coordinate(double lat, double lon) {
if (lat < -90 || lat > 90) throw new IllegalArgumentException("Invalid lat");
if (lon < -180 || lon > 180) throw new IllegalArgumentException("Invalid lon");
this.lat = lat;
this.lon = lon;
}
}
Records can have instance methods, static methods, and static fields. They cannot have instance fields beyond their components.
record Money(long cents, String currency) {
// Static factory method
public static Money of(double amount, String currency) {
return new Money(Math.round(amount * 100), currency);
}
// Instance method
public Money add(Money other) {
if (!this.currency.equals(other.currency))
throw new IllegalArgumentException("Currency mismatch");
return new Money(this.cents + other.cents, this.currency);
}
// Custom toString (overrides auto-generated)
@Override
public String toString() {
return String.format("%s %.2f", currency, cents / 100.0);
}
// Static constant — allowed
public static final Money ZERO_USD = new Money(0, "USD");
}
Money price = Money.of(19.99, "USD");
Money tax = Money.of(1.60, "USD");
System.out.println(price.add(tax)); // USD 21.59
Records can implement interfaces but cannot extend classes (they implicitly extend java.lang.Record). This makes records excellent for implementing value-object interfaces.
interface Shape {
double area();
double perimeter();
}
record Circle(double radius) implements Shape {
public double area() { return Math.PI * radius * radius; }
public double perimeter() { return 2 * Math.PI * radius; }
}
record Rectangle(double width, double height) implements Shape {
public double area() { return width * height; }
public double perimeter() { return 2 * (width + height); }
}
// Works great with sealed interfaces for exhaustive pattern matching
List<Shape> shapes = List.of(new Circle(5), new Rectangle(3, 4));
shapes.forEach(s -> System.out.printf("Area=%.2f%n", s.area()));
Records also work seamlessly with Comparable and other standard interfaces:
record Version(int major, int minor, int patch)
implements Comparable<Version> {
@Override
public int compareTo(Version other) {
int c = Integer.compare(this.major, other.major);
if (c != 0) return c;
c = Integer.compare(this.minor, other.minor);
return c != 0 ? c : Integer.compare(this.patch, other.patch);
}
}
var versions = List.of(
new Version(2, 1, 0),
new Version(1, 9, 3),
new Version(2, 0, 1)
);
versions.stream().sorted().forEach(System.out::println);
// Version[major=1, minor=9, patch=3]
// Version[major=2, minor=0, patch=1]
// Version[major=2, minor=1, patch=0]
- Use a record when the class is a pure data carrier — DTOs, value objects, result types, event objects, coordinates, ranges.
- Use a regular class when you need mutable state, inheritance, lazy initialization, or complex construction logic.
- Records cannot extend other classes and cannot be abstract.
- Records are not suitable as JPA/Hibernate entities (no no-arg constructor, no setters).
- Records are Serializable if you implement the interface — but custom serialization logic (readObject/writeObject) is not allowed.
// Classic DTO use case — a record replaces 30+ lines of boilerplate
record UserDto(long id, String username, String email) {}
// API response wrapper
record ApiResponse<T>(T data, String status, String message) {
public static <T> ApiResponse<T> ok(T data) {
return new ApiResponse<>(data, "OK", null);
}
public static <T> ApiResponse<T> error(String message) {
return new ApiResponse<>(null, "ERROR", message);
}
}
ApiResponse<UserDto> resp = ApiResponse.ok(new UserDto(1, "alice", "alice@example.com"));
System.out.println(resp);
// ApiResponse[data=UserDto[id=1, username=alice, email=alice@example.com], status=OK, message=null]
Records cannot have instance fields beyond their components. Any state beyond what is declared in the component list is not allowed.