Records
Records (since Java 16, finalized in Java 17) are a concise way to declare immutable data classes. The compiler automatically generates constructors, accessors, equals(), hashCode(), and toString() — eliminating boilerplate for data-centric types.
Basic Record
public record Point(int x, int y) { }
Point p = new Point(3, 4);
System.out.println(p.x()); // 3 (accessor, not getX())
System.out.println(p.y()); // 4
System.out.println(p); // Point[x=3, y=4]
System.out.println(p.equals(new Point(3, 4))); // true
A record implicitly:
- Is
finaland cannot be extended - Has
private finalfields for each component - Provides a public canonical constructor matching the components
- Provides accessor methods named after components (no
getprefix) - Generates
equals(),hashCode(), andtoString()based on all components
Canonical Constructor
The compiler generates a constructor matching the component list. You can add validation with a compact constructor:
public record Range(int start, int end) {
public Range {
if (start > end) {
throw new IllegalArgumentException(
"start (" + start + ") must be <= end (" + end + ")");
}
}
}
The compact constructor assigns fields automatically after it runs — no need to write this.start = start.
You can also define a non-compact constructor for alternative construction paths:
public record Email(String address) {
public Email(String address) {
this.address = address.toLowerCase().trim();
}
}
Custom Methods and Static Members
Records can include additional methods, static fields, and static factory methods:
public record Rectangle(double width, double height) {
public double area() {
return width * height;
}
public double perimeter() {
return 2 * (width + height);
}
public static Rectangle square(double side) {
return new Rectangle(side, side);
}
public static final Rectangle UNIT = new Rectangle(1, 1);
}
Keep records focused on data — complex behavior belongs in service classes.
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);
}
}
// Sorting
List<Person> people = List.of(
new Person("Alice", 30),
new Person("Bob", 25)
);
people.stream().sorted().forEach(System.out::println);
Records work well as implementations of sealed interfaces:
public sealed interface Shape permits Circle, Rectangle { }
public record Circle(double radius) implements Shape { }
public record Rectangle(double width, double height) implements Shape { }
Records vs Classes
| Feature | Record | Class |
|---|---|---|
| Purpose | Immutable data carrier | General-purpose object |
| Mutability | Immutable by design | Mutable by default |
| Inheritance | Cannot extend classes (extends Record) |
Can extend one class |
| Boilerplate | Minimal | Manual equals/hashCode/toString |
| Fields | All final components | Any modifier |
| Abstract | Cannot be abstract | Can be abstract |
Local Records (Java 16+)
Records can be declared inside methods for scoped data grouping:
public void processOrders(List<Order> orders) {
record OrderSummary(String id, double total) { }
var summaries = orders.stream()
.map(o -> new OrderSummary(o.id(), o.calculateTotal()))
.toList();
}
Limitations
- Cannot extend other classes (implicitly extends
java.lang.Record) - Cannot declare instance fields beyond the record components
- Are implicitly
final— cannot be extended - Cannot be abstract
- Cannot have instance initializers (use compact constructor instead)
- Sensitive components appear in
toString()— override if needed
When to Use Records
| Use Case | Example |
|---|---|
| DTOs | record UserDto(Long id, String email, String role) |
| Value objects | record Money(BigDecimal amount, Currency currency) |
| Multi-return values | record SearchResult(List<Item> items, int totalCount) |
| Map keys | Records with proper equals/hashCode |
| API responses | record ApiResponse(int status, String body) |
public record UserDto(Long id, String email, String role) { }
// Safe as map key
Map<UserDto, List<Order>> ordersByUser = new HashMap<>();
ordersByUser.put(new UserDto(1L, "[email protected]", "admin"), List.of());
Serialization and JSON
Records work with Jackson, Gson, and JSON-B when configured properly:
// Jackson deserializes via canonical constructor
public record Product(String name, BigDecimal price) { }
// JSON: {"name":"Widget","price":9.99}
ObjectMapper mapper = new ObjectMapper();
Product p = mapper.readValue(json, Product.class);
Ensure JSON property names match record component names (or use annotations).
Best Practices
- Keep records focused on data — put complex behavior in separate service classes
- Use compact constructors for validation and normalization
- Prefer records over Lombok
@Valuefor simple immutable data types - Override
toString()if components contain sensitive data (passwords, tokens) - Use records with sealed interfaces for algebraic data types
- Avoid making every class a record — mutable entities with identity (JPA entities) still need classes
Troubleshooting
| Issue | Cause | Fix |
|---|---|---|
| “invalid accessor method” | Trying to name accessor getX() |
Use component name: x() not getX() |
| Jackson deserialization fails | Missing canonical constructor match | Align JSON fields with component names |
| Cannot add mutable field | Records only allow components | Use a regular class instead |
equals surprises with arrays |
Array components compared by reference | Use List instead of array components |
| Record not immutable | Mutable component type (e.g., List) |
Wrap with List.copyOf() in constructor |
Records reduce boilerplate for the data-heavy patterns that dominate modern Java applications — DTOs, value objects, and sealed type variants — while maintaining full immutability and correct equality semantics.