A record is a class designed to represent data through a fixed set of components.
Before records, a simple data class required a lot of boilerplate code.
You want to create a simple class to store a Product (name and price). Nothing else. Just data.
To do it “properly” in classic Java, you had to write:
- Private attributes.
- The constructor.
- Getters (and maybe setters).
- The
equals()method (for comparisons). - The
hashCode()method (for use in maps). - The
toString()method (so printing doesn’t show garbage).
Result: 50 lines of code for something that should take 1. It’s boring, clutters the project, and is error-prone.
No more! With the arrival of Records, Java solves this from the ground up.
What is a record?
A record is a special type of class designed specifically to be an immutable data carrier.
The premise is: “If you tell me what data you want, I (the compiler) will generate all the boring code for you.”
The comparison: class vs record
Look at this. It’s the same thing, written in two ways.
Classic approach (POJO):
public class Product {
private final String name;
private final double price;
public Product(String name, double price) {
this.name = name;
this.price = price;
}
public String getName() { return name; }
public double getPrice() { return price; }
@Override
public String toString() { return "Product[name=" + name + ", price=" + price + "]"; }
// ... Imagine 20 more lines of equals() and hashCode() here ...
}Modern approach (Record):
public record Product(String name, double price) { }Yes. A single line. That’s all.
What happens under the hood?
When you compile that record, Java automatically generates for you:
- Canonical constructor: Receives all components (
name,price). - Private and final attributes: The data is immutable. They cannot be changed once the object is created.
- Accessor methods: Note! They aren’t called
getName(), they are named the same as the attribute:name()andprice(). equals()andhashCode(): Perfectly implemented for value-based comparison (two products with the same name and price will be equal).toString(): A nice implementation that prints the values.
public static void main(String[] args) {
Product p1 = new Product("Laptop", 999.99);
Product p2 = new Product("Laptop", 999.99);
// Data access (without 'get')
System.out.println(p1.name());
// Content comparison (thanks to the generated equals)
System.out.println(p1.equals(p2)); // true
// Print
System.out.println(p1); // Output: Product[name=Laptop, price=999.99]
}Immutability: The key restriction
Records are designed to be Immutable.
- They have no setters.
- Their fields are
final.
If you want to change a product’s price, you can’t. You have to create a new record with the new price (usually by copying the data from the previous one).
This, which seems like a limitation, is a blessing for concurrent programming and avoiding state bugs. Records are ideal for DTOs (Data Transfer Objects), API responses, or Map keys.
Customizing a record
Although Java generates everything, sometimes we need to add extra logic.
Validation (compact constructor)
Imagine we want to prevent the price from being negative.
Records have a special syntax called the Compact Constructor. You don’t need to repeat the parameters or the assignments (this.x = x). You just write the validation logic.
public record Product(String name, double price) {
// Compact Constructor: Runs BEFORE assigning the values
public Product {
if (price < 0) {
throw new IllegalArgumentException("Price cannot be negative");
}
if (name == null || name.isBlank()) {
name = "No Name"; // We can even "normalize" data
}
}
}Extra methods
You can add your own methods if needed.
public record Product(String name, double price) {
public double priceWithVAT() {
return this.price * 1.21;
}
}