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

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 class
public 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

Pro Tips

📋 Quick Reference

TaskSyntaxNotes
Declarerecord Point(int x, int y) {}Generates everything
Createnew Point(3, 4)Canonical constructor
Read a valuep.x()No "get" prefix
ValidatePoint { ...checks... }Compact constructor
Add a methoddouble area() { ... }Derived values allowed
Implementrecord 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

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.

Related lessons