Browse Clojure Foundations for Java Developers

Immutability in Java

See what Java teams have to build manually to get trustworthy immutable models, and why that discipline still matters when moving to Clojure.

Java can absolutely be written in an immutable style.

Many strong Java teams already do some version of it:

  • final fields
  • constructors instead of setters
  • immutable value objects
  • defensive copies around mutable collections
  • APIs that return new objects instead of mutating old ones

That background helps when learning Clojure. The difference is not that Java cannot do immutability. The difference is that Java makes you enforce it deliberately, while Clojure gives it to you by default.

What “Immutable” Usually Means In Java

An immutable Java object normally follows a short checklist:

Rule Why it matters
Fields are private Callers cannot mutate them directly
Fields are final Construction is the main point where values are assigned
No setters The public API does not expose mutation
Mutable inputs are copied Callers cannot retain a handle and mutate shared state later
Mutable fields are returned defensively Accessors do not leak writable internals

That is a respectable engineering discipline. It is also a lot of discipline.

A Real Java Example

Here is a version of a Java value object that is genuinely trying to stay immutable:

 1import java.math.BigDecimal;
 2import java.util.List;
 3
 4public final class Invoice {
 5    private final String id;
 6    private final List<LineItem> items;
 7    private final BigDecimal discount;
 8
 9    public Invoice(String id, List<LineItem> items, BigDecimal discount) {
10        this.id = id;
11        this.items = List.copyOf(items);
12        this.discount = discount;
13    }
14
15    public String id() {
16        return id;
17    }
18
19    public List<LineItem> items() {
20        return items;
21    }
22
23    public BigDecimal discount() {
24        return discount;
25    }
26
27    public Invoice withDiscount(BigDecimal newDiscount) {
28        return new Invoice(id, items, newDiscount);
29    }
30}

This is good Java.

It also shows the cost:

  • constructor ceremony
  • accessor methods
  • explicit copy policy
  • “with” methods or builders for every meaningful update path

For one class, that is manageable. For a system full of nested models, the surface area adds up quickly.

Shallow Immutability Is Still A Trap

Java developers also learn an important hard lesson: final is not enough by itself.

If a field points to a mutable object, the reference may be stable while the object still changes.

 1public final class Schedule {
 2    private final List<String> tasks;
 3
 4    public Schedule(List<String> tasks) {
 5        this.tasks = tasks; // unsafe
 6    }
 7
 8    public List<String> tasks() {
 9        return tasks; // unsafe
10    }
11}

That object is not truly immutable, because callers can still mutate the shared List.

This is why Java immutability often turns into a policy problem:

  • which types are actually safe?
  • where do we copy?
  • are wrappers enough?
  • which code path leaked a mutable collection?

Those questions are not academic. They are where many bugs come from.

Records Help, But They Do Not Solve Everything

Java records reduce a lot of boilerplate for data carriers, which is genuinely helpful.

But records do not magically deep-freeze their components. If a record field holds mutable state, the record is still only as immutable as its contents.

That means the same old questions still apply:

  • are the components immutable?
  • did we copy mutable inputs?
  • does any accessor leak a mutable object?

Records improve ergonomics. They do not change the core burden.

Why This Matters For A Java Engineer Learning Clojure

If you already value immutable Java design, you are not starting from zero.

You already understand:

  • why mutable aliases are risky
  • why defensive copies exist
  • why thread safety gets easier with stable values
  • why returning a new value can be cleaner than mutating in place

What changes in Clojure is that you stop rebuilding this discipline class by class.

The language and standard collections start from value semantics instead.

Where Java Immutability Still Shines

Be careful not to overcorrect. Java immutability is still worth learning and using well because:

  • you will still interoperate with Java libraries from Clojure
  • many JVM boundaries still expose mutable types
  • disciplined Java modeling makes migration easier

The better framing is:

  • Java immutability is a carefully constructed design style
  • Clojure immutability is the baseline operating model

That is a meaningful difference.

Knowledge Check

### Which combination is closest to a genuinely immutable Java object? - [x] Private final fields, no setters, and defensive handling of mutable inputs - [ ] Public fields and synchronized setters - [ ] Final fields only, regardless of what they point to - [ ] A mutable class wrapped in a singleton > **Explanation:** `final` alone is not enough. Real immutability in Java depends on API discipline and careful handling of mutable internals. ### Why can a Java object with final fields still fail to be truly immutable? - [x] Because the referenced objects may still be mutable - [ ] Because final fields are ignored by the JVM - [ ] Because constructors always imply mutation - [ ] Because Java does not support object creation > **Explanation:** A stable reference does not guarantee an immutable object graph. The contents still matter. ### What do Java records improve most directly? - [x] Boilerplate for data carriers - [ ] Deep immutability of all nested objects - [ ] Automatic transactional state management - [ ] Lock-free concurrency for every field > **Explanation:** Records reduce ceremony, but they do not automatically make mutable components safe. ### Why is disciplined Java immutability still useful when moving to Clojure? - [x] It builds the same value-semantics intuition you will rely on in Clojure and at Java interop boundaries - [ ] It makes Clojure atoms unnecessary - [ ] It replaces persistent data structures - [ ] It removes the need to learn `assoc` and `update` > **Explanation:** Good Java immutability habits transfer well, especially around aliasing, stable values, and safer APIs.
Revised on Friday, April 24, 2026