Records
A record (introduced as a standard feature in Java 16) is a concise way to declare an immutable data-carrier class — a class whose entire purpose is to hold a fixed set of values. What used to take twenty or thirty lines of repetitive boilerplate can be expressed in a single line.
A record in one line
record Point(int x, int y) {}That single declaration automatically generates: a constructor that takes x and y, accessor methods, equals(), hashCode(), and a readable toString() — all without you writing a single line of their bodies.
Accessors: x() not getX()
Records deliberately break from the traditional JavaBean naming convention. Instead of getX(), the generated accessor for field x is simply x() — named exactly after the field, with no get prefix.
Using a record
Point p = new Point(3, 4); System.out.println(p.x()); // 3, not p.getX() System.out.println(p.y()); // 4 System.out.println(p); // Point[x=3, y=4] — generated toString() Point p2 = new Point(3, 4); System.out.println(p.equals(p2)); // true — generated equals() compares fields System.out.println(p.hashCode() == p2.hashCode()); // true
Point[x=3, y=4] true true
Before vs. after: the boilerplate records eliminate
To achieve the exact same behavior — immutable fields, a constructor, accessors, a value-based equals()/hashCode(), and a readable toString() — as a traditional class, you'd need something like this:
The traditional, hand-written equivalent
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) {
if (this == o) return true;
if (!(o instanceof Point)) return false;
Point other = (Point) o;
return x == other.x && y == other.y;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
@Override
public String toString() {
return "Point[x=" + x + ", y=" + y + "]";
}
}Records are immutable by design
A record class is implicitly final — it cannot be subclassed — and every one of its fields is implicitly final too. There is no way to reassign x or y on an existing Point; if you need a “changed” point, you construct a brand-new one.
Compact constructors for validation
Sometimes you need to validate incoming values before they're stored. A compact constructor lets you do that without repeating the parameter list or the field assignments — the standard assignments still happen automatically after your validation code runs.
Validating in a compact constructor
record Point(int x, int y) {
Point { // compact constructor — no parameter list, no explicit assignments
if (x < 0 || y < 0) {
throw new IllegalArgumentException("Coordinates must be non-negative: (" + x + ", " + y + ")");
}
// this.x = x; this.y = y; still happen automatically after this block
}
}
public class Main {
public static void main(String[] args) {
Point valid = new Point(3, 4); // fine
Point invalid = new Point(-1, 4); // throws IllegalArgumentException
}
}record Point(int x, int y) {}generates a constructor, accessors,equals(),hashCode(), andtoString().Accessors are named after the field —
x(), notgetX().Records are implicitly
final, and every field is implicitlyfinal— immutable by design.A compact constructor validates or normalizes input without repeating field assignments.