AI InterviewSearch…Search problems, topics, or pages…Ctrl KGet started
Skip to content

10 Enums & records / Lesson 4 of 5

Records — transparent immutable data

14 min · +10 XP · 2 exercises · 5 questions

Read the text14 min
Exercises0/2
Quiz5 q

Quick answer

What is a record in Java?

A record in Java, standard since Java 16, is a class declared as a transparent, immutable carrier of data: record Point(int x, int y) {}. From that header the compiler generates a private final field per component, a canonical constructor, accessors named x() and y(), and equals, hashCode and toString over all components. Records are final and cannot extend another class.

Most classes that hold data — a point, a pair, a DTO, a parse result — need the same forty lines: private final fields, a constructor, accessors, equals, hashCode, toString. Java 16's records generate all of it from one line, and in doing so make a promise the compiler enforces: a record is a transparent, immutable carrier of its components. This lesson covers what a record generates, what you can customise, and the rules that keep the promise.

Declaring a record

public record Point(int x, int y) { }

Point p = new Point(3, 4);
p.x()            // 3   — accessor, named after the component (no "get")
p.y()            // 4
p.toString()     // "Point[x=3, y=4]"
p.equals(new Point(3, 4))   // true — component-wise
p.hashCode()     // combined from x and y, consistent with equals

The header (int x, int y) declares the components. From it the compiler generates:

  • a private final field per component;
  • a canonical constructor taking all components in order;
  • a public accessor per component, named exactly like it;
  • equals, hashCode and toString over all components.

Records are implicitly final, extend java.lang.Record (so cannot extend anything else), and may implement interfaces. They can be top-level, nested (implicitly static) or local — declared inside a method, handy for a temporary pair in an algorithm.

What you can add

public record Range(int lo, int hi) implements Comparable<Range> {

    // compact canonical constructor: validate / normalise; assignment is implicit at the end
    public Range {
        if (lo > hi) throw new IllegalArgumentException(lo + " > " + hi);
    }

    // extra constructors must delegate to the canonical one
    public Range(int single) { this(single, single); }

    // instance methods
    public int length() { return hi - lo; }
    public boolean contains(int x) { return lo <= x && x < hi; }

    // static members and factories
    public static Range empty() { return new Range(0, 0); }
    public static final Range UNIT = new Range(0, 1);

    // an accessor may be overridden (rarely wise); its signature is fixed
    @Override public int lo() { return lo; }

    @Override public int compareTo(Range o) { return Integer.compare(lo, o.lo); }
}

The compact constructor has no parameter list; it runs before the fields are assigned and may reassign the parameters (lo = Math.min(lo, hi)) to normalise them. It cannot assign this.lo directly — the assignment happens automatically after the body.

What you cannot do

  • No instance fields beyond the components. private int cache; in a record is a compile error. Static fields are fine.
  • No setters, no mutation of components: the fields are final. "Modifying" means creating a new record: new Point(p.x() + 1, p.y()) — a withX(int) method is the idiom if you want it.
  • No extends; records may not be abstract; components cannot be volatile/transient in the usual sense.
  • Accessor names are fixed to the component names; a getX() alongside is legal but pointless.

Equality is by value, always

equals compares each component with == for primitives (Double.compare semantics for float/double) and equals for references. Two records with equal components are equal — which is what a data carrier should mean, and what makes records good map keys and set elements without any hand-written code. Caveat: an array component uses Object.equals on the array (identity), so record Bag(int[] items) compares arrays by reference — use a List instead.

Records and collections: shallow immutability

public record Team(String name, List<String> members) {
    public Team {
        members = List.copyOf(members);      // defensive, immutable copy in the compact constructor
    }
}

A record's fields are final; the objects they refer to are whatever they are. A List component can be mutated by whoever holds the list unless you copy it on the way in. List.copyOf produces an immutable copy (and rejects nulls); for maps Map.copyOf. Make this a habit for every collection or array component.

Records as tuples and multi-value returns

record MinMax(int min, int max) { }
static MinMax minMax(int[] a) { … return new MinMax(lo, hi); }
MinMax r = minMax(values);
r.min(); r.max();

The answer to "how do I return two values" (Module 5) is a record — named, typed, and one line to declare, even locally inside the method that needs it.

Records with sealed interfaces

sealed interface Shape permits Circle, Square { }
record Circle(double r) implements Shape { }
record Square(double side) implements Shape { }

Records are the natural leaves of a sealed hierarchy (Module 9): final, immutable, with generated equality. Java 21's record patterns destructure them in a switch: case Circle(double r) -> Math.PI * r * r.

Records versus classes

NeedUse
Immutable data with value equalityrecord
Mutable state, identity, lifecycleclass
Inheritance from a classclass (records cannot extend)
Extra hidden state or caching fieldsclass (records forbid instance fields)
JPA entities, JavaBeans frameworks needing settersclass
DTOs, keys, tuples, messages, configuration valuesrecord

Records are not "classes with less typing"; they are a semantic declaration that the type is nothing but its components. If that is not true — if two instances with equal components should be distinguishable, or the state changes — it is not a record.

Serialization, reflection and frameworks

Records serialise through their canonical constructor (safer than field-poking), and Class.getRecordComponents() exposes the components to frameworks. Jackson, Gson and most modern libraries support records directly. Older frameworks that require a no-arg constructor and setters do not.

Interview angle

  • "What does a record generate?" Final fields, canonical constructor, accessors, equals/hashCode/toString.
  • "Can a record have a mutable field?" No instance fields besides the components, all final; the referenced objects may be mutable — copy them in.
  • "Can a record extend a class?" No; it can implement interfaces.
  • "What is a compact constructor?" The canonical constructor without a parameter list, for validation and normalisation before the implicit assignments.
  • "Are records immutable?" Shallowly: components cannot be reassigned; deep immutability depends on the component types.

Key takeaways

  • record Name(T a, U b) generates fields, constructor, accessors a()/b(), and value-based equals/hashCode/toString.
  • Customise with a compact constructor (validate, normalise), extra constructors delegating to the canonical one, methods, statics, interfaces.
  • No extra instance fields, no setters, no extends; equality is by components — array components compare by reference.
  • Copy collection components defensively (List.copyOf) for real immutability.
  • Use records for data; classes for identity and mutation.

Common questions

What is a compact constructor in a Java record?

A compact constructor is the record's canonical constructor written without a parameter list, as in public Range { … }. It runs before the fields are assigned, so it can validate the parameters, throw on bad input or reassign them to normalise values; the fields are then set automatically from the parameters.

Are Java records immutable?

Shallowly. A record's fields are final and it has no setters, but a component that refers to a mutable object, such as a List, can still be changed by whoever holds that object. Copy such components in the compact constructor with List.copyOf for real immutability.

Can a record extend a class in Java?

No. Every record implicitly extends java.lang.Record and is final, so it can neither extend another class nor be extended. A record can implement any number of interfaces, which is how it joins a sealed hierarchy or becomes Comparable.

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

A record declares that a type is nothing but its components: immutable, compared by value, with everything generated. Use a class when the type has identity, mutable state, hidden fields such as a cache, a superclass, or a framework that needs a no-argument constructor and setters.

Can a record have instance fields in Java?

No — only the fields generated from its components. Declaring another instance field is a compile error, though static fields, static methods, instance methods and extra constructors that delegate to the canonical one are all allowed.

Mark as readSign in to keep your place.

Java Records Explained: Compact Constructors and Immutability

A Java record is a one-line immutable data class that generates its constructor, accessors, equals, hashCode and toString. Compact constructors and the rules.

  • Course: Java study plan
  • Module: Enums & records
  • Kind: Lesson
  • Reading time: 14 min
  • Runtime: OpenJDK 18

What is a record in Java?

A record in Java, standard since Java 16, is a class declared as a transparent, immutable carrier of data: record Point(int x, int y) {}. From that header the compiler generates a private final field per component, a canonical constructor, accessors named x() and y(), and equals, hashCode and toString over all components. Records are final and cannot extend another class.

Lesson

Most classes that hold data — a point, a pair, a DTO, a parse result — need the same forty lines: private final fields, a constructor, accessors, equals, hashCode, toString. Java 16's records generate all of it from one line, and in doing so make a promise the compiler enforces: a record is a transparent, immutable carrier of its components. This lesson covers what a record generates, what you can customise, and the rules that keep the promise.

Declaring a record

public record Point(int x, int y) { }

Point p = new Point(3, 4);
p.x()            // 3   — accessor, named after the component (no "get")
p.y()            // 4
p.toString()     // "Point[x=3, y=4]"
p.equals(new Point(3, 4))   // true — component-wise
p.hashCode()     // combined from x and y, consistent with equals

The header (int x, int y) declares the components. From it the compiler generates:

  • a private final field per component;
  • a canonical constructor taking all components in order;
  • a public accessor per component, named exactly like it;
  • equals, hashCode and toString over all components.

Records are implicitly final, extend java.lang.Record (so cannot extend anything else), and may implement interfaces. They can be top-level, nested (implicitly static) or local — declared inside a method, handy for a temporary pair in an algorithm.

What you can add

public record Range(int lo, int hi) implements Comparable<Range> {

    // compact canonical constructor: validate / normalise; assignment is implicit at the end
    public Range {
        if (lo > hi) throw new IllegalArgumentException(lo + " > " + hi);
    }

    // extra constructors must delegate to the canonical one
    public Range(int single) { this(single, single); }

    // instance methods
    public int length() { return hi - lo; }
    public boolean contains(int x) { return lo <= x && x < hi; }

    // static members and factories
    public static Range empty() { return new Range(0, 0); }
    public static final Range UNIT = new Range(0, 1);

    // an accessor may be overridden (rarely wise); its signature is fixed
    @Override public int lo() { return lo; }

    @Override public int compareTo(Range o) { return Integer.compare(lo, o.lo); }
}

The compact constructor has no parameter list; it runs before the fields are assigned and may reassign the parameters (lo = Math.min(lo, hi)) to normalise them. It cannot assign this.lo directly — the assignment happens automatically after the body.

What you cannot do

  • No instance fields beyond the components. private int cache; in a record is a compile error. Static fields are fine.
  • No setters, no mutation of components: the fields are final. "Modifying" means creating a new record: new Point(p.x() + 1, p.y()) — a withX(int) method is the idiom if you want it.
  • No extends; records may not be abstract; components cannot be volatile/transient in the usual sense.
  • Accessor names are fixed to the component names; a getX() alongside is legal but pointless.

Equality is by value, always

equals compares each component with == for primitives (Double.compare semantics for float/double) and equals for references. Two records with equal components are equal — which is what a data carrier should mean, and what makes records good map keys and set elements without any hand-written code. Caveat: an array component uses Object.equals on the array (identity), so record Bag(int[] items) compares arrays by reference — use a List instead.

Records and collections: shallow immutability

public record Team(String name, List<String> members) {
    public Team {
        members = List.copyOf(members);      // defensive, immutable copy in the compact constructor
    }
}

A record's fields are final; the objects they refer to are whatever they are. A List component can be mutated by whoever holds the list unless you copy it on the way in. List.copyOf produces an immutable copy (and rejects nulls); for maps Map.copyOf. Make this a habit for every collection or array component.

Records as tuples and multi-value returns

record MinMax(int min, int max) { }
static MinMax minMax(int[] a) { … return new MinMax(lo, hi); }
MinMax r = minMax(values);
r.min(); r.max();

The answer to "how do I return two values" (Module 5) is a record — named, typed, and one line to declare, even locally inside the method that needs it.

Records with sealed interfaces

sealed interface Shape permits Circle, Square { }
record Circle(double r) implements Shape { }
record Square(double side) implements Shape { }

Records are the natural leaves of a sealed hierarchy (Module 9): final, immutable, with generated equality. Java 21's record patterns destructure them in a switch: case Circle(double r) -> Math.PI * r * r.

Records versus classes

NeedUse
Immutable data with value equalityrecord
Mutable state, identity, lifecycleclass
Inheritance from a classclass (records cannot extend)
Extra hidden state or caching fieldsclass (records forbid instance fields)
JPA entities, JavaBeans frameworks needing settersclass
DTOs, keys, tuples, messages, configuration valuesrecord

Records are not "classes with less typing"; they are a semantic declaration that the type is nothing but its components. If that is not true — if two instances with equal components should be distinguishable, or the state changes — it is not a record.

Serialization, reflection and frameworks

Records serialise through their canonical constructor (safer than field-poking), and Class.getRecordComponents() exposes the components to frameworks. Jackson, Gson and most modern libraries support records directly. Older frameworks that require a no-arg constructor and setters do not.

Interview angle

  • "What does a record generate?" Final fields, canonical constructor, accessors, equals/hashCode/toString.
  • "Can a record have a mutable field?" No instance fields besides the components, all final; the referenced objects may be mutable — copy them in.
  • "Can a record extend a class?" No; it can implement interfaces.
  • "What is a compact constructor?" The canonical constructor without a parameter list, for validation and normalisation before the implicit assignments.
  • "Are records immutable?" Shallowly: components cannot be reassigned; deep immutability depends on the component types.

Key takeaways

  • record Name(T a, U b) generates fields, constructor, accessors a()/b(), and value-based equals/hashCode/toString.
  • Customise with a compact constructor (validate, normalise), extra constructors delegating to the canonical one, methods, statics, interfaces.
  • No extra instance fields, no setters, no extends; equality is by components — array components compare by reference.
  • Copy collection components defensively (List.copyOf) for real immutability.
  • Use records for data; classes for identity and mutation.

Common questions

What is a compact constructor in a Java record?

A compact constructor is the record's canonical constructor written without a parameter list, as in public Range { … }. It runs before the fields are assigned, so it can validate the parameters, throw on bad input or reassign them to normalise values; the fields are then set automatically from the parameters.

Are Java records immutable?

Shallowly. A record's fields are final and it has no setters, but a component that refers to a mutable object, such as a List, can still be changed by whoever holds that object. Copy such components in the compact constructor with List.copyOf for real immutability.

Can a record extend a class in Java?

No. Every record implicitly extends java.lang.Record and is final, so it can neither extend another class nor be extended. A record can implement any number of interfaces, which is how it joins a sealed hierarchy or becomes Comparable.

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

A record declares that a type is nothing but its components: immutable, compared by value, with everything generated. Use a class when the type has identity, mutable state, hidden fields such as a cache, a superclass, or a framework that needs a no-argument constructor and setters.

Can a record have instance fields in Java?

No — only the fields generated from its components. Declaring another instance field is a compile error, though static fields, static methods, instance methods and extra constructors that delegate to the canonical one are all allowed.

Exercises

Points as records

Declare record Point(int x, int y) with a method double distanceTo(Point o) and a static factory origin(). Read an integer n and n pairs x y. Put every point into a HashSet<Point> and print distinct=<size>; print hasOrigin=<set contains Point.origin()>; then print each distinct point in input order (use a LinkedHashSet) as its toString() followed by its distance to the origin with two decimals.

Example: 3 then 3 4, 0 0, 3 4 →

distinct=2
hasOrigin=true
Point[x=3, y=4] 5.00
Point[x=0, y=0] 0.00

Rational numbers as records

Write record Rational(long num, long den) whose compact constructor rejects a zero denominator (IllegalArgumentException), moves any sign to the numerator and reduces by the gcd — so new Rational(2, -4) becomes -1/2. Add plus and times returning new records, and toString as num/den (just num when den == 1). Read n lines a b op c d and print a/b op c/d = <result>; record equality then works by value: also print equal=<result.equals(new Rational(…))> where the second is the same fraction unreduced, given on the line as e f.

Example: 1 2 + 1 3 5 6 (one line, n = 1) →

1/2 + 1/3 = 5/6 equal=true

In this module: Enums & records

← EnumSet and EnumMap · Records in practice — validation, wither methods and modelling →