Skip to content

Java Records

Updated 8 min read Aimerscrew

A Java record is a special kind of class built for one job: holding a few pieces of data. Before records, a simple class for a point on a map needed fields, a constructor, getters, equals(), hashCode() and toString(), often 40 lines of code that said almost nothing. With a record, it's one line. In this lesson, you'll learn what Java generates for you, how to add validation with a compact constructor, why records are immutable, and when a normal class is still the better choice.

🌍 Real-life example

Think of a printed receipt. It lists the item, the price and the quantity, and once it's printed, nobody edits it. If the price changes, the store prints a new receipt. Records work the same way: a fixed bundle of values that you read but never change.

What is a record in Java?

A record is a class declared with the record keyword, followed by a list of components in parentheses. Each component is a piece of data the record carries. Records became a standard feature in Java 16, so they work on any Java 17+ setup.

public class MapPins {
    public static void main(String[] args) {
        Point cafe = new Point(3, 4);
        Point library = new Point(3, 4);
        Point park = new Point(-2, 7);

        System.out.println(cafe);                       // toString()
        System.out.println("x = " + cafe.x() + ", y = " + cafe.y());   // accessors
        System.out.println("cafe.equals(library): " + cafe.equals(library));
        System.out.println("cafe == library: " + (cafe == library));
        System.out.println("cafe.equals(park): " + cafe.equals(park));
    }
}

record Point(int x, int y) { }
Point[x=3, y=4]
x = 3, y = 4
cafe.equals(library): true
cafe == library: false
cafe.equals(park): false

That single line, record Point(int x, int y) { }, gave us a constructor, two accessor methods, a readable toString(), and an equals() that compares the data. cafe and library are still two separate objects (== is false), but they're equal by value.

What does Java generate for a record?

For every component, the compiler writes the boring code you learned in encapsulation and the Object class lessons:

Animated diagram: on the left, the one line you write, record Point(int x, int y) { }. On the right, the members Java generates appear one by one: private final fields x and y, a constructor Point(int x, int y), accessor methods x() and y(), equals(Object o), hashCode() and toString(), which returns Point[x=3, y=4]
You write one line; the compiler writes the rest.
Generated memberFor record Point(int x, int y)
Fieldsprivate final int x; and private final int y;
Canonical constructorPoint(int x, int y), which sets both fields
Accessorsx() and y() (no get prefix)
equals()True when all components are equal
hashCode()Built from all components, matching equals()
toString()Point[x=3, y=4]

The canonical constructor is simply the constructor that takes every component in order. Notice that the accessors are named x(), not getX().

📝 Note

Because equals() and hashCode() follow the contract automatically, records work perfectly as keys in a HashMap or items in a HashSet. No more forgetting hashCode().

Adding methods to a record

The curly braces after the components are a class body. You can add normal methods, static fields and static methods there. Let's model a line in a shopping cart:

public class CartLine {
    public static void main(String[] args) {
        Product headphones = new Product("  Wireless Headphones ", 59.99, 2);
        Product cable = new Product("USB-C Cable", 9.50, 3);

        System.out.println(headphones);
        System.out.printf("%s: $%.2f%n", headphones.name(), headphones.lineTotal());
        System.out.printf("%s: $%.2f%n", cable.name(), cable.lineTotal());
        System.out.printf("Cart total: $%.2f%n", headphones.lineTotal() + cable.lineTotal());
    }
}

record Product(String name, double price, int quantity) {

    // Compact constructor: runs before the fields are set
    Product {
        if (price < 0) {
            throw new IllegalArgumentException("Price can't be negative: " + price);
        }
        if (quantity < 1) {
            throw new IllegalArgumentException("Quantity must be at least 1");
        }
        name = name.trim();          // clean up the value before it is stored
    }

    // Records can have normal methods too
    double lineTotal() {
        return price * quantity;
    }
}
Product[name=Wireless Headphones, price=59.99, quantity=2]
Wireless Headphones: $119.98
USB-C Cable: $28.50
Cart total: $148.48

The extra spaces around " Wireless Headphones " disappeared, because the record cleaned up the name before storing it. That happened in the compact constructor.

How do compact constructors validate data?

A compact constructor is a special constructor for records, written with just the record name and no parameter list: Product { ... }. The parameters are there implicitly, with the same names as the components. Its code runs first, and then Java assigns the (possibly changed) parameters to the fields automatically.

It's the perfect place to validate data, so a record can never hold nonsense. If someone tries new Product("Mug", -5, 1), the throw statement stops the program with a clear message:

Exception in thread "main" java.lang.IllegalArgumentException: Price can't be negative: -5.0

throw raises an error on purpose. You'll learn all about it in Java throw and throws; for now, just know it refuses bad data.

💡 Tip

In a compact constructor, assign to the parameter (name = name.trim();), not to this.name. Java copies the parameters into the fields after your code finishes.

Records are immutable

A record's fields are private final, and there are no setters. Once a record is created, its data never changes. An object like this is called immutable. To "change" a value, you create a new record, often with a small helper method:

public class PriceChange {
    public static void main(String[] args) {
        Product original = new Product("Desk Lamp", 24.00);
        Product onSale = original.withPrice(18.00);

        System.out.println("Original: " + original);
        System.out.println("On sale:  " + onSale);
        // original.price = 18.00;   // compile error: price is final
    }
}

record Product(String name, double price) {
    Product withPrice(double newPrice) {
        return new Product(name, newPrice);   // a new record; the old one is unchanged
    }
}
Original: Product[name=Desk Lamp, price=24.0]
On sale:  Product[name=Desk Lamp, price=18.0]

Writing original.price = 18.00 from outside fails with price has private access in Product, and even inside the record you'd get cannot assign a value to final variable price. Immutable objects are easy to reason about: you can share them anywhere without worrying that some other code will change them behind your back.

⚠️ Be careful

A record is only "shallowly" immutable. If a component is a mutable object, such as an array, the record can't be pointed at a different array, but the array's contents can still change. Prefer immutable component types like String, numbers and other records.

Records in a HashSet

Because equality is built in, records are great for things like coordinates or keys. Here a game remembers which grid cells a player has visited:

import java.util.HashSet;
import java.util.Set;

public class Visitors {
    public static void main(String[] args) {
        Set<GridCell> visited = new HashSet<>();
        visited.add(new GridCell(2, 5));
        visited.add(new GridCell(2, 5));   // equal record: not added again
        visited.add(new GridCell(0, 1));

        System.out.println("Cells visited: " + visited.size());
        System.out.println("Been to (2, 5)? " + visited.contains(new GridCell(2, 5)));
    }
}

record GridCell(int row, int col) { }
Cells visited: 2
Been to (2, 5)? true

Compare this with the Seat class in the previous lesson, which needed about 25 lines for the same behavior.

Record vs class: what's the difference?

RecordNormal class
Main purposeCarry dataData plus behavior and changing state
FieldsAlways private final, from the componentsAny fields you declare
Extra instance fieldsNot allowed (only static)Allowed
Setters / changing dataNo, immutableYes, if you want
equals, hashCode, toStringGenerated automaticallyWrite them yourself
Can extend a class?No (records are implicitly final)Yes
Can implement interfaces?YesYes
Accessor namesname()Usually getName()

When should you not use a record?

  • The data must change over time. A bank account balance goes up and down; a game character moves. Use a normal class with encapsulated fields.
  • You need inheritance. Records can't extend another class, and nothing can extend a record.
  • You want to hide some of the data. Every component gets a public accessor and appears in toString(). A record holding a password would print it.
  • Equality isn't "all fields equal". If two books are equal when only the ISBN matches, a record's generated equals() is wrong for you (you could override it, but then a class is usually clearer).

Common mistakes with records

1. Calling getX() instead of x()

Record accessors use the component name: point.x(). point.getX() gives cannot find symbol.

2. Adding an instance field

record Product(String name, double price) { int stock; } fails with field declaration must be static. All instance data must be a component in the parentheses.

3. Trying to extend a class

record Point(int x, int y) extends Shape doesn't compile. Records already extend java.lang.Record. Implement an interface instead.

4. Expecting a setter

There is no setPrice(). Create a new record with the new value, as withPrice() did above.

Practice exercise

🏋️ Try it yourself

A weather station records the temperature in different cities. Create a record Temperature(String city, double celsius) with a compact constructor that rejects values below absolute zero (-273.15), and a method fahrenheit() that returns celsius * 9 / 5 + 32. Print readings for London (14.0), Dubai (38.5) and Oslo (-6.0) in both units, then print the first record itself.

Show solution
public class WeatherStation {
    public static void main(String[] args) {
        Temperature[] readings = {
            new Temperature("London", 14.0),
            new Temperature("Dubai", 38.5),
            new Temperature("Oslo", -6.0)
        };

        for (Temperature t : readings) {
            System.out.printf("%s: %.1f C = %.1f F%n", t.city(), t.celsius(), t.fahrenheit());
        }
        System.out.println(readings[0]);
    }
}

record Temperature(String city, double celsius) {
    Temperature {
        if (celsius < -273.15) {
            throw new IllegalArgumentException("Below absolute zero: " + celsius);
        }
    }

    double fahrenheit() {
        return celsius * 9 / 5 + 32;
    }
}
London: 14.0 C = 57.2 F
Dubai: 38.5 C = 101.3 F
Oslo: -6.0 C = 21.2 F
Temperature[city=London, celsius=14.0]

What's next?

So far, every class, record and enum has lived side by side at the top level of a file. Java also lets you put a class inside another class. Next, learn when and why to do that in Java inner classes. Python has a similar idea called dataclasses, if you'd like to compare.

Key takeaways

  • A record, like record Point(int x, int y) { }, is a compact class for carrying data (standard since Java 16).
  • Java generates private final fields, a canonical constructor, accessors such as x(), and equals, hashCode and toString.
  • A compact constructor, written without a parameter list, is the place to validate or clean up data.
  • Records are immutable: no setters, and fields cannot be reassigned; create a new record to change a value.
  • Records can have methods, static members and implement interfaces, but cannot extend a class or add instance fields.
  • Use a normal class when data must change, when you need inheritance, or when some fields should stay hidden.

Frequently asked questions

What is a record in Java?

A record is a special class declared with the record keyword that holds a fixed set of values called components. Java automatically generates its fields, constructor, accessor methods, equals, hashCode and toString.

Which Java version introduced records?

Records were previewed in Java 14 and 15 and became a standard, permanent feature in Java 16. They work in Java 17, 21 and every later version.

Are Java records immutable?

Yes. Record fields are private and final and there are no setters, so values cannot change after creation. However, if a component is a mutable object such as an array, that object's contents can still change.

What is a compact constructor in a Java record?

It is a constructor written with only the record name and no parameter list, for example Product { ... }. It runs before the fields are assigned, so it is used to validate or normalize the incoming values.

Can a Java record extend another class?

No. Every record implicitly extends java.lang.Record and is final, so it cannot extend another class and cannot be extended. A record can implement any number of interfaces.

What is the difference between a record and a class in Java?

A record is a shortcut for an immutable data carrier with generated methods. A normal class needs those methods written by hand but can have changing state, extra fields, setters and inheritance.