Encodera

Thanks for reading!

Donate to the site — we work hard for you.

How to create an immutable class

Short answer

A record is the shortest way. Its fields are final, and the constructor, accessors, equals(), hashCode() and toString() are generated automatically:

record Money(String currency, long cents) {}
 
void main() {
    var price = new Money("EUR", 1999);
 
    IO.println(price);
    IO.println(price.cents());
}

Output:

Money[currency=EUR, cents=1999]
1999

Validation in the constructor

A record's compact constructor checks the values before they are assigned to the fields:

record Money(String currency, long cents) {
    Money {
        if (cents < 0) {
            throw new IllegalArgumentException("cents must not be negative: " + cents);
        }
    }
}
 
void main() {
    IO.println(new Money("EUR", 500));
    new Money("EUR", -1);
}

Output:

Money[currency=EUR, cents=500]
Exception in thread "main" java.lang.IllegalArgumentException: cents must not be negative: -1

A change means a new object

Instead of setters, add methods that return a new object:

record Money(String currency, long cents) {
    Money plus(long more) {
        return new Money(currency, cents + more);
    }
}
 
void main() {
    var price = new Money("EUR", 1000);
    var total = price.plus(250);
 
    IO.println(price);
    IO.println(total);
}

Output:

Money[currency=EUR, cents=1000]
Money[currency=EUR, cents=1250]

Collections inside

A record stores a reference to the list it was given. Make an unmodifiable copy in the constructor:

record Team(String name, List<String> members) {
    Team {
        members = List.copyOf(members);
    }
}
 
void main() {
    var people = new ArrayList<>(List.of("Anna"));
    var team = new Team("Blue", people);
    people.add("Oleg");
 
    IO.println(team.members());
}

Output:

[Anna]

A regular class

Without a record: the class is final, all fields are private final, and there are no setters:

final class Point {
    private final int x;
    private final int y;
 
    Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
 
    int x() { return x; }
    int y() { return y; }
}
 
void main() {
    var p = new Point(3, 4);
    IO.println(p.x() + ", " + p.y());
}

Output:

3, 4

Common mistake

Storing a mutable list without a copy. Outside code changes the "immutable" object:

record Team(String name, List<String> members) {}
 
void main() {
    var people = new ArrayList<>(List.of("Anna"));
    var team = new Team("Blue", people);
    people.add("Oleg");
 
    IO.println(team.members());
}

Output:

[Anna, Oleg]

Functions used in this recipe

  • record — Declares a record — an immutable data class with ready-made equals(), hashCode() and toString().
  • final — Forbids reassigning a variable, overriding a method or extending a class.
  • List.copyOf() — Creates an unmodifiable list copy of a collection.

Thanks for reading!

Donate to the site — we work hard for you.