Records in Java
A record declares an immutable data carrier, and the compiler generates the constructor, accessors, equals, hashCode and toString.
- Definition
- What the compiler generates
- The same thing without a record
- Validation with a compact constructor
- Adding behaviour
- Rules and restrictions
- Shallow immutability
- Records with pattern matching
- Local and nested records
- When not to use a record
- Common mistakes
- Best practices
- Practice
- Conclusion
-
Java Basics
- Introduction to Java
- Setting Up Java and Writing Your First Program
- Variables, Data Types and Literals in Java
- Type Casting and Type Conversion in Java
- Operators and Expressions in Java
- Input and Output in Java
- Comments, Keywords and Naming Conventions in Java
- Control Flow in Java: if, else and switch
- Loops in Java: for, while and do-while
- Methods
- Arrays and Strings
-
OOP
- Classes and Objects in Java
- Constructors in Java
- The this Keyword in Java
- The static Keyword in Java
- Encapsulation in Java
- Access Modifiers in Java
- Inheritance in Java
- Method Overriding and super in Java
- Polymorphism in Java
- Abstraction, Abstract Classes and Interfaces in Java
- Composition, Aggregation and Association in Java
- The Object Lifecycle in Java
- Core Java
- Exception Handling
-
Collections
- The Java Collections Framework
- List in Java: ArrayList, LinkedList, Vector and Stack
- Set in Java: HashSet, LinkedHashSet and TreeSet
- Map in Java: HashMap, LinkedHashMap and TreeMap
- How HashMap Works Internally in Java
- Queue and Deque in Java: ArrayDeque and PriorityQueue
- Iterators in Java
- Comparable and Comparator in Java
- Collections Utilities and Choosing the Right Collection
- Generics
- Java 8+
- Stream API
- Date and Time
- File and I/O
-
Multithreading
- Threads in Java: Processes, Runnable and Thread
- Thread Lifecycle in Java
- Synchronization in Java: synchronized and volatile
- Locks and Atomic Classes in Java
- Race Conditions and Deadlocks in Java
- The Executor Framework and Thread Pools in Java
- Future and CompletableFuture in Java
- Concurrent Collections in Java
- The Java Memory Model
- JVM and Memory
- Advanced Java
- Networking
- JDBC
- Testing
Definition
A record is a class whose purpose is to carry data. You declare the components, and the compiler supplies the rest. Records were finalised in Java 16.
public record Point(int x, int y) { }What the compiler generates
- A
private finalfield per component. - A canonical constructor taking every component in order.
- An accessor per component, named after it:
x(), notgetX(). equalsandhashCodebuilt from all components.toStringlisting every component.
Point a = new Point(3, 4);
Point b = new Point(3, 4);
System.out.println(a.x()); // 3
System.out.println(a); // Point[x=3, y=4]
System.out.println(a.equals(b)); // true
System.out.println(a.hashCode() == b.hashCode()); // trueThe same thing without a record
public final class Point {
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; }
@Override public boolean equals(Object o) {
return o instanceof Point p && p.x == x && p.y == y;
}
@Override public int hashCode() { return Objects.hash(x, y); }
@Override public String toString() { return "Point[x=" + x + ", y=" + y + "]"; }
}Around thirty lines replaced by one, with no chance of the two equality methods drifting apart.
Validation with a compact constructor
public record Money(String currency, long minorUnits) {
public Money { // compact: no parameter list, no assignments
Objects.requireNonNull(currency, "currency is required");
if (minorUnits < 0) {
throw new IllegalArgumentException("Amount must not be negative");
}
currency = currency.toUpperCase(); // normalisation is allowed
}
}The compact form runs before the fields are assigned, so reassigning a parameter normalises the stored value. This is where every validation rule belongs.
Adding behaviour
public record Rectangle(double width, double height) {
public double area() {
return width * height;
}
public Rectangle scaled(double factor) {
return new Rectangle(width * factor, height * factor); // return a new one
}
public static Rectangle square(double side) {
return new Rectangle(side, side);
}
}A record may declare methods, static members and additional constructors. It simply may not add instance fields beyond its components.
Rules and restrictions
| Rule | Reason |
|---|---|
Implicitly final | Value semantics must not be undermined by a subclass |
| Cannot extend a class | It already extends java.lang.Record |
| May implement interfaces | Contracts are still useful |
| No extra instance fields | The state is exactly the components |
Components are final | Records are shallowly immutable |
Shallow immutability
public record Team(String name, List<String> members) {
public Team {
members = List.copyOf(members); // defensive copy on the way in
}
}Without the copy, a caller keeps a reference to the list and can still modify it after construction. A record freezes the reference, not the object behind it, exactly as final does.
Records with pattern matching
sealed interface Shape permits Circle, Rectangle { }
record Circle(double radius) implements Shape { }
record Rectangle(double width, double height) implements Shape { }
static double area(Shape shape) {
return switch (shape) {
case Circle(double r) -> Math.PI * r * r;
case Rectangle(double w, double h) -> w * h;
};
}Records, sealed types and pattern matching were designed together. A sealed hierarchy of records gives the compiler enough information to check that every case is handled.
Local and nested records
public List<String> topNotes(List<Note> notes) {
record Scored(Note note, double score) { } // local to this method
return notes.stream()
.map(note -> new Scored(note, rank(note)))
.sorted(Comparator.comparingDouble(Scored::score).reversed())
.map(scored -> scored.note().title())
.toList();
}When not to use a record
- The object needs mutable state.
- The identity matters more than the values, as with a database entity.
- The fields are an implementation detail that should stay hidden; a record exposes all of them.
- You need to extend a class.
Common mistakes
- Expecting
getX(). The accessor isx(). - Assuming deep immutability and storing a mutable collection without copying.
- Writing the full canonical constructor and then repeating assignments the compact form would have handled.
- Using a record for an entity whose identity is a database key rather than its contents.
Best practices
- Use records for DTOs, value objects, method results and map keys.
- Put validation and normalisation in the compact constructor.
- Copy mutable components in and out.
- Combine records with sealed interfaces to model a closed set of alternatives.
- Give a record behaviour when the behaviour belongs to the data.
Practice
- Convert a class with four fields, getters,
equals,hashCodeandtoStringinto a record. - Add validation to a
Percentagerecord that rejects values outside zero to one hundred. - Show that a record holding a
Listis not deeply immutable, then fix it. - Write a sealed interface with two record implementations and an exhaustive pattern switch.
- Give one situation where a record would be the wrong choice, and explain why.
Conclusion
A record states that a type is its data. The compiler then writes the boilerplate correctly and consistently, leaving you to add validation and any behaviour that genuinely belongs with the values.