Browse Clojure Foundations for Java Developers

Mutable Data Structures in Java

Review the Java collection habits Clojure is reacting against: in-place updates, shared aliases, defensive copying, and synchronization pressure.

Java’s mutable collections are not a mistake. They are a sensible fit for a language and ecosystem built around object identity, encapsulated state, and in-place updates.

But to understand why Clojure feels so different, you need to see the costs that come with that default.

What “Mutable” Really Means In Practice

A mutable collection can change after it is created.

With Java collections, that usually means methods like:

  • add
  • remove
  • put
  • set
  • clear

modify the same collection instance in place.

1List<String> statuses = new ArrayList<>();
2statuses.add("draft");
3statuses.add("pending");
4statuses.set(1, "paid");

After set, the original statuses object has changed. Any code holding a reference to that same list now observes different state.

That is the key point. Mutation is rarely just “a local update.” It changes what shared references mean over time.

Why Java Developers Get Used To This

Mutable collections fit naturally with common Java patterns:

  • entities with setters
  • service methods that update aggregates
  • builders that accumulate values
  • caches and registries stored in maps
  • frameworks that hydrate and mutate objects

A lot of Java APIs are designed around this expectation. It is normal to pass a collection into a method and have that method populate, mutate, or sort it.

That design style is so common that it can feel invisible until you move into a value-oriented language like Clojure.

The Main Convenience Of Mutation

The appeal is obvious:

  • in-place updates are direct
  • incremental algorithms feel natural
  • you can maintain identity while fields change
  • many APIs become straightforward to express imperatively

That convenience is real. Clojure is not pretending otherwise.

The issue is that the convenience has a cost structure.

Aliasing Is The First Hidden Cost

The biggest problem with mutable structures is not the method names. It is aliasing.

If two parts of a program hold the same mutable collection, either part can affect the other accidentally.

1Map<String, Integer> inventory = new HashMap<>();
2inventory.put("A-1", 10);
3
4Map<String, Integer> sharedView = inventory;
5sharedView.put("A-1", 0);
6
7System.out.println(inventory.get("A-1")); // 0

Nothing exotic happened here. Two references point to the same object, so one update changes the reality seen by both.

In a small program, that may be manageable. In a large codebase, aliasing becomes a constant source of uncertainty.

Defensive Copying Becomes A Habit

Because mutable collections can be shared and changed unexpectedly, Java developers often reach for defensive copying:

1List<OrderItem> safeItems = new ArrayList<>(incomingItems);

This helps, but it introduces new concerns:

  • when exactly should you copy?
  • how deep does the copy need to be?
  • who owns the copied version now?
  • are nested objects still mutable?

Defensive copying is often necessary in Java, but it is also a signal that the default data model does not preserve trust by itself.

Concurrency Raises The Cost Further

Once multiple threads can touch the same mutable structure, the reasoning burden jumps.

Now you must think about:

  • synchronization
  • visibility
  • atomicity
  • iteration safety
  • lock discipline

Java gives you tools for this:

  • synchronized
  • locks
  • concurrent collections
  • atomics

Those tools are important, but they exist partly because mutation is ordinary and widely available.

Temporal Coupling Is Another Quiet Problem

Mutable designs often create logic that depends on a particular sequence of operations:

1Order order = new Order();
2order.setCustomer(customer);
3order.addItem(itemA);
4order.addItem(itemB);
5order.calculateTotals();
6order.markReady();

To know whether this is correct, you need to understand:

  • what each method changes
  • whether the call order matters
  • which methods assume earlier mutation already happened

That is a different kind of complexity from concurrency, but it comes from the same underlying idea: identity changes over time.

None Of This Means “Never Mutate”

It is important to stay honest here.

Mutable structures are still a good fit for some Java tasks:

  • tight local algorithms
  • low-level performance work
  • UI models
  • interop-heavy code
  • APIs designed around mutation

The lesson is not that Java is wrong to have mutable collections.

The lesson is that once you understand the trade-offs clearly, Clojure’s choice to make immutable persistent collections the default stops feeling arbitrary.

Why This Matters Before Learning The Clojure Side

If you skip the Java mental model, Clojure immutability can sound like a restriction.

Once you remember the real cost of mutable defaults, the Clojure trade becomes easier to see:

  • less in-place convenience
  • more trust in values
  • less aliasing risk
  • simpler sharing
  • clearer data flow

That is a serious engineering trade, not an ideology badge.

Knowledge Check

### What is the most important practical consequence of mutable Java collections? - [x] The same collection instance can mean different things over time because updates happen in place - [ ] They cannot hold nested data - [ ] They always require reflection - [ ] They are automatically thread-safe > **Explanation:** Mutation changes the meaning of a shared object over time, which is why aliasing and hidden state changes become important. ### Why do Java developers often use defensive copying? - [x] To protect against unexpected changes through shared mutable references - [ ] Because Java collections are immutable by default - [ ] To avoid the JVM garbage collector - [ ] Because `HashMap` cannot be read directly > **Explanation:** Defensive copying is a response to the fact that mutable objects can be changed through aliases held elsewhere in the program. ### Why does concurrency make mutable collections harder to manage? - [x] Multiple threads can read and write the same structure, so you must coordinate visibility and updates - [ ] Mutable collections cannot be used on the JVM - [ ] Threads stop supporting collections - [ ] Because every `ArrayList` creates its own thread > **Explanation:** Once mutation is shared across threads, synchronization and coordination become necessary to preserve correctness. ### What is temporal coupling in this context? - [x] Behavior that depends on a particular sequence of in-place mutations happening in the right order - [ ] A requirement that all methods run at the same time - [ ] A feature of immutable maps - [ ] The JVM clock changing during execution > **Explanation:** Mutable designs often become order-sensitive because objects change state across multiple method calls.
Revised on Friday, April 24, 2026