Records
Reviewed & published by Brayan K
For decades, a simple Java class that just held a few values meant pages of boilerplate — fields, a constructor, getters, equals, hashCode, toString. Records (Java 16+) collapse all of that into a single line, giving you immutable, correct data classes for free.
Part of the free Java course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
Before You Start
You should know classes, constructors, and fields, and have met equals and hashCode — records generate both for you, so seeing the manual version first makes the magic obvious.
What You'll Learn
- Declare a record and what it generates for you
- How records compare to the old boilerplate class
- Why records are immutable and value-based
- Accessors named after components (x(), not getX())
- Compact constructors for validation
- Adding extra methods, factories, and interfaces
The Big Idea — A Pre-Filled Form
💡 Analogy: Writing a data class the old way is like filling in a long form by hand — name field, address field, signature, witness — for every single document. A record is a pre-filled template: you state the fields once on the dotted line (record Point(int x, int y)) and the rest — constructor, accessors, equality, printing — is stamped in automatically. Less to write, nothing to get wrong.
The components you list in the parentheses are the record's whole identity. Two records are .equals() when their components are equal — value-based, not reference-based — which is exactly what you want for data.
1️⃣ Declaring a Record
A record declaration lists its components in parentheses. From that one line Java generates: private final fields, a canonical constructor, an accessor per component, and equals/hashCode/toString.
record Point(int x, int y) {} // that's the entire classpublic class Main {
// ONE LINE replaces a whole hand-written class. This record gives you:
// - private final fields x and y
// - a constructor Point(int x, int y)
// - accessor methods x() and y()
// - equals(), hashCode(), and toString()
record Point(int x, int y) {}
public static void main(String[] args) {
Point p = new Point(3, 4);
// Accessors are named after the components (no "get" prefix):
System.out.println("x = " + p.x());
System.out.println("y = " + p.y());
// Free toString():
System.out.println("p = " + p);
// Free, value-based equals() and hashCode():
Point q = new Point(3, 4);
System.out.println("p.equals(q)? " + p.equals(q));
System.out.println("same hashCode? " + (p.hashCode() == q.hashCode()));
}
}2️⃣ The Boilerplate It Replaces
To appreciate records, look at what you'd otherwise hand-write — and have to keep in sync every time you add a field. A record does all of this, correctly, in one line.
public class Main {
// The OLD way — about 40 lines for a simple data holder:
static final class PointOld {
private final int x;
private final int y;
PointOld(int x, int y) { this.x = x; this.y = y; }
public int getX() { return x; }
public int getY() { return y; }
@Override public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof PointOld)) return false;
PointOld p = (PointOld) o;
return x == p.x && y == p.y;
}
@Override public int hashCode() { return 31 * x + y; }
@Override public String toString() {
return "PointOld[x=" + x + ", y=" + y + "]";
}
}
// The NEW way — one line, identical behaviour:
record PointNew(int x, int y) {}
public static void main(String[] args) {
System.out.println(new PointOld(1, 2));
System.out.println(new PointNew(1, 2));
System.out.println("Equal contents, same idea, 1 line vs ~20.");
}
}3️⃣ Validation & Extra Methods
Records aren't just dumb holders. A compact constructor lets you validate or normalise inputs — note it has no parameter list and no field assignments; you only write the checks, and Java assigns the fields afterwards.
record Range(int lo, int hi) {
Range { // compact constructor
if (lo > hi) throw new IllegalArgumentException("lo > hi");
} // fields assigned automatically
}You can also add normal methods, derived values, static factory methods, and implement interfaces (including Comparable). What you can't do: add extra instance fields or extend another class.
public class Main {
// A COMPACT CONSTRUCTOR validates inputs without re-listing parameters.
// No assignments needed — Java assigns the fields for you afterwards.
record Temperature(double celsius) {
Temperature {
if (celsius < -273.15) {
throw new IllegalArgumentException("Below absolute zero: " + celsius);
}
}
// Records can have EXTRA methods, including derived values:
double fahrenheit() { return celsius * 9 / 5 + 32; }
// ...and static factory methods:
static Temperature freezing() { return new Temperature(0); }
}
public static void main(String[] args) {
Temperature t = new Temperature(25);
System.out.println(t.celsius() + "C = " + t.fahrenheit() + "F");
System.out.println("Freezing point: " + Temperature.freezing());
try {
new Temperature(-300);
} catch (IllegalArgumentException e) {
System.out.println("Rejected: " + e.getMessage());
}
}
}4️⃣ Records in Action — Sorting
Records shine with the rest of modern Java. Because each component has a clean accessor, method references like Product::price plug straight into a Comparator. This is the combination you'll meet again in the checkpoint.
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class Main {
record Product(String name, double price) {}
public static void main(String[] args) {
List<Product> cart = new ArrayList<>(List.of(
new Product("Pen", 1.50),
new Product("Bag", 9.99),
new Product("Pad", 3.25)
));
// Records + method references read beautifully together:
cart.sort(Comparator.comparingDouble(Product::price));
System.out.println("Cart sorted by price:");
double total = 0;
for (Product p : cart) {
System.out.printf(" %-4s $%.2f%n", p.name(), p.price());
total += p.price();
}
System.out.printf(" Total: $%.2f%n", total);
}
}🧩 Reorder Challenge
These lines declare a record with a compact constructor and use it. Put them in the right order.
A) }
B) record Percent(int value) {
C) System.out.println(new Percent(80));
D) Percent {
E) if (value < 0 || value > 100) throw new IllegalArgumentException("0-100 only");
F) }Correct order: B, D, E, F, A, C
record Percent(int value) {
Percent {
if (value < 0 || value > 100) throw new IllegalArgumentException("0-100 only");
}
}
System.out.println(new Percent(80));
// Output: Percent[value=80]Open the record header, open the compact constructor (no parameter list!), add the validation, close the constructor, close the record, then use it.
🧠 Quick Recall
Predict the output before opening each answer.
record Coord(int x, int y) {}
Coord a = new Coord(1, 2);
Coord b = new Coord(1, 2);
System.out.println(a.equals(b) + " " + a);Answer: true Coord[x=1, y=2]. Records get value-based equals for free, so two coords with the same components are equal, and the generated toString prints the component names and values.
record User(String name) {}
User u = new User("Ada");
u.name = "Bob";Answer: No. A record component becomes a private final field with no setter, so u.name = "Bob" fails to compile (cannot assign a value to final variable name, and the field isn't accessible anyway). Records are immutable — to "change" one, create a new instance.
record Money(int cents) {}
Money m = new Money(150);
// which line reads the value correctly?
// A) m.getCents() B) m.cents C) m.cents()Answer: C — m.cents(). The accessor is named after the component with no "get" prefix and is a method call, so m.getCents() doesn't exist and m.cents (the field) is private.
🎯 Your Turn #1 — Declare a record
Write a one-line Book record with the right components so the main method compiles.
public class Main {
// YOUR TURN: declare a record named Book with two components:
// String title (a String)
// int pages (an int)
// Then the main method below should compile and run as-is.
// Expected output:
// Clean Code by ... has 464 pages
// Equal? true
// declare your record here
public static void main(String[] args) {
Book b1 = new Book("Clean Code", 464);
Book b2 = new Book("Clean Code", 464);
System.out.println(b1.title() + " ... has " + b1.pages() + " pages");
System.out.println("Equal? " + b1.equals(b2));
}
}🎯 Your Turn #2 — Validate with a compact constructor
Add a compact constructor that rejects a negative quantity.
public class Main {
// YOUR TURN: add a compact constructor to this record that rejects a
// negative quantity by throwing IllegalArgumentException.
// Expected output:
// OrderLine[item=Widget, quantity=3]
// Rejected: quantity must be >= 0
record OrderLine(String item, int quantity) {
// add the compact constructor here
}
public static void main(String[] args) {
System.out.println(new OrderLine("Widget", 3));
try {
new OrderLine("Gadget", -1);
} catch (IllegalArgumentException e) {
System.out.println("Rejected: " + e.getMessage());
}
}
}🧩 Mini-Challenge — Contact book
Model contacts with a record, then sort and print them with aligned columns.
import java.util.*;
public class Main {
// GOAL: model a contact book with a record, then sort and print.
//
// Expected output:
// Contacts (by last name, then first):
// Jones, Ada ada@x.com
// Smith, Bob bob@x.com
// Smith, Cara cara@x.com
// 1. declare: record Contact(String first, String last, String email) {}
// 2. build a List<Contact> with the three people below
// 3. sort by last name, then first name
// 4. print each as " Last, First email" (use printf for alignment)
public static void main(String[] args) {
// data to use:
// Smith / Bob / bob@x.com
// Jones / Ada / ada@x.com
// Smith / Cara / cara@x.com
// your code here
}
}Common Errors
- ❌ Calling getX() on a record. The accessor is x(), not getX(). Fix: use the component name as the method name.
- ❌ Trying to add a mutable field. error: field declaration must be static — records can't add instance fields beyond their components. Fix: add the value as a component, or use a normal class.
- ❌ Re-listing parameters in a compact constructor. Writing Range(int lo, int hi) {...} makes it a canonical constructor needing manual assignments. Fix: use the compact form Range { ... } with no parameter list.
- ❌ Trying to extend a class. error: records cannot extend — a record implicitly extends java.lang.Record. Fix: records can implement interfaces, but not extend a class.
- ❌ Assuming deep immutability. A record with a List component still lets callers mutate that list. Fix: use immutable components, or defensively copy in the compact constructor.
- ❌ Using records on Java 15 or earlier. They're a preview in 14–15 and final from 16. Fix: compile with Java 16+.
Pro Tips
- 💡 Reach for a record whenever a class is "just its data" — DTOs, API responses, value objects, multi-value method returns.
- 💡 Add static factory methods with meaningful names (Temperature.freezing()) for common values instead of magic constructors.
- 💡 Defensive-copy mutable components in the compact constructor (list = List.copyOf(list);) to keep true immutability.
- 💡 Records pair perfectly with pattern matching and sealed types — together they make clean, modern domain models in newer Java.
📋 Quick Reference
| Task | Syntax | Notes |
|---|---|---|
| Declare | record Point(int x, int y) {} | Generates everything |
| Create | new Point(3, 4) | Canonical constructor |
| Read a value | p.x() | No "get" prefix |
| Validate | Point { ...checks... } | Compact constructor |
| Add a method | double area() { ... } | Derived values allowed |
| Implement | record R(...) implements I {} | Interfaces only, no extends |
🎉 Lesson Complete!
You can now declare records, know exactly what they generate, why they're immutable and value-based, how to validate with a compact constructor, and how to add your own methods. You've replaced pages of error-prone boilerplate with a single, correct line.
Practice quiz
Which does a record NOT generate automatically?
- A canonical constructor
- Accessor methods
- Setter methods
- equals, hashCode, and toString
Answer: Setter methods. Records are immutable, so they generate accessors but never setters.
For 'record Point(int x, int y) {}', how do you read x?
- p.getX()
- p.x
- p.x()
- p.getx()
Answer: p.x(). Accessors are named after the component with no 'get' prefix: p.x().
What does the generated toString() print for new Point(3, 4)?
- Point(3, 4)
- Point[x=3, y=4]
- {x:3, y:4}
- 3, 4
Answer: Point[x=3, y=4]. The generated toString prints Point[x=3, y=4].
Are two records equal when their components are equal?
- No, equals is reference-based
- Yes — equals is value-based
- Only if the same instance
- Only for one component
Answer: Yes — equals is value-based. Records get value-based equals: same components means equal.
A record component becomes what kind of field?
- public mutable
- private final
- protected
- static
Answer: private final. Each component becomes a private final field — records are immutable.
What is a compact constructor used for?
- Adding setters
- Validating or normalising inputs without re-listing parameters
- Extending a class
- Adding new fields
Answer: Validating or normalising inputs without re-listing parameters. A compact constructor validates inputs; Java assigns the fields afterward.
Which can a record do?
- Extend another class
- Add extra mutable instance fields
- Implement an interface
- Have public non-final fields
Answer: Implement an interface. A record can implement interfaces but cannot extend a class or add extra instance fields.
Can you reassign a record component after construction, e.g. u.name = "Bob"?
- Yes
- No — the field is private final
- Only inside main
- Only with a setter
Answer: No — the field is private final. Components are private final with no setter, so reassignment fails to compile.
In which Java version did records become a final (non-preview) feature?
- Java 8
- Java 11
- Java 16
- Java 21
Answer: Java 16. Records were a preview in 14-15 and final from Java 16.
Why is a record's immutability described as 'shallow'?
- It only applies to ints
- A mutable component (like a List) can still have its contents changed
- Records can be reassigned
- Only the first field is final
Answer: A mutable component (like a List) can still have its contents changed. The reference can't change, but a mutable component's contents still can — prefer immutable components.
Continue this course
- Previous: equals() & hashCode()
- Next: Comparable & Comparator — Sort objects your way with Comparable and Comparator
- Quick reference: Java cheat sheet
Frequently asked questions
What is a record in Java?
A record is a compact way to declare an immutable data-carrier class, added in Java 16. Writing record Point(int x, int y) {} automatically generates private final fields, a canonical constructor, accessor methods (x() and y()), and correct equals, hashCode, and toString. It replaces dozens of lines of boilerplate for the common case of 'a class that just holds some values'.
Are records immutable?
Yes. Every component becomes a private final field, and there are no setters, so once a record is constructed its values can't change. This makes records inherently thread-safe and safe to use as map keys. Note the immutability is shallow — if a component is itself a mutable object (like an ArrayList), the reference can't change but the list's contents still can, so prefer immutable components.
How are a record's accessors named?
They're named exactly after the components, with no 'get' prefix: for record Point(int x, int y) you call point.x() and point.y(), not getX(). This is a deliberate departure from the JavaBean getX() convention. If you need bean-style getters for a framework, you'll have to add them manually or rely on a framework that understands records.
Can a record have validation or extra methods?
Yes. Use a compact constructor — record Range(int lo, int hi) { Range { if (lo > hi) throw ...; } } — to validate or normalise inputs without re-listing the parameters or assigning fields (Java assigns them for you afterwards). You can also add ordinary instance methods, static methods, static factory methods, and implement interfaces. What you can't do is add extra mutable instance fields or extend another class.
When should I use a record instead of a normal class?
Use a record when the class's identity is its data — DTOs, value objects, coordinates, API responses, the return of a method that needs to bundle several values. Use a regular class when you need mutability, inheritance from another class, lifecycle/behaviour beyond holding data, or hidden internal state. A good test: if the class is 'just its fields', a record is the right tool.