Java Records (Java 14+) — Immutable Data Carriers
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 finalfieldsxandy- A canonical constructor
Point(double x, double y) - Accessor methods
x()andy()(notgetX()) equals()based on all fieldshashCode()based on all fieldstoString()→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
- Records eliminate boilerplate for immutable data classes.
- Auto-generated: constructor, accessors (
x()notgetX()),equals,hashCode,toString. - Use compact constructors for validation and normalisation.
- Records can implement interfaces but cannot extend classes.
- Use defensive copies for mutable component types.
- 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.