Back to Blog
Java

Java Mutable vs Immutable: When to Use Each

java mutable vs immutable: Understand the difference between mutable and immutable objects in Java, and learn when to use each for thread safety, performance, and main...

JavaImmutabilityMutable ObjectsThread SafetyJava Collections
A visual comparison of mutable and immutable Java objects, showing state changes versus fixed state.

When you design a Java class, one of the first decisions is whether instances should be mutable or immutable. The choice affects thread safety, performance, and how the rest of your code interacts with the objects. In this article, we'll compare java mutable vs immutable by looking at how each behaves, where they differ in practice, and how to decide which fits your use case.

What Mutable and Immutable Mean in Java

A mutable object can change its state after construction. An immutable object cannot. In Java, the distinction is not enforced by the language itself; it depends on how you write the class.

Consider String and StringBuilder. String is immutable: every operation that appears to modify it, like concat or replace, returns a new String instance. StringBuilder is mutable: methods like append modify the existing object's internal state.

String s = "hello"; s.concat(" world"); // returns new String, original unchanged System.out.println(s); // "hello" StringBuilder sb = new StringBuilder("hello"); sb.append(" world"); // modifies sb in place System.out.println(sb); // "hello world"

The same principle applies to your own classes. If a class exposes only getters and never changes its fields after construction, it is effectively immutable. If it has setters or methods that mutate internal fields, it is mutable.

How Java Enforces Immutability

To make a class truly immutable, you need to follow several rules. The most important is to declare all fields final. This guarantees that the field is assigned exactly once, in the constructor, and cannot be reassigned later. You also need to ensure that no method can change the object's state, so you avoid setters and any methods that modify fields.

Here is a typical immutable class:

public final class Point { private final int x; private final int y; public Point(int x, int y) { this.x = x; this.y = y; } public int getX() { return x; } public int getY() { return y; } public Point translate(int dx, int dy) { return new Point(x + dx, y + dy); } }

The translate method does not modify the existing Point; it returns a new instance. This is the core pattern of immutability: every state change produces a fresh object.

Making the class final prevents subclasses from overriding methods and introducing mutability. If you need to share an immutable object across threads, you can do so safely because no thread can observe a different state.

The Cost of Immutability: Allocation and Performance

The main practical downside of immutability is allocation. Every change requires a new object, which means more garbage collection pressure. For objects that are updated frequently, such as a counter or a buffer, the overhead of creating many short-lived objects can be significant.

However, immutability also enables optimizations that are impossible with mutable objects. Because an immutable object never changes, you can safely cache its hash code, reuse the same instance across threads, and share internal structures. For example, the String class caches its hash code, and String literals are interned.

The performance tradeoff depends on the usage pattern. If you have a large object that is updated in a tight loop, mutable is often more efficient. If you have small value objects that are created once and read many times, immutability adds little overhead and improves reliability.

Concurrency and Thread Safety

Immutable objects are inherently thread-safe. Since their state cannot change, multiple threads can access them without synchronization. This eliminates entire classes of concurrency bugs, such as race conditions and visibility issues.

Mutable objects, on the other hand, require careful synchronization. If two threads modify the same object without proper locking, the result is unpredictable. Even reading a mutable object while another thread writes to it can produce stale or inconsistent data.

Consider a simple counter:

public class Counter { private int value; public void increment() { value++; } public int getValue() { return value; } }

If two threads call increment() concurrently, the value++ operation is not atomic. You need synchronized or AtomicInteger to make it safe. An immutable counter would instead return a new counter instance with the incremented value, which is naturally safe to share.

This does not mean you should make every class immutable. Mutable objects are still useful when you need to accumulate state over time, such as a builder or a collection that is built incrementally.

When to Choose Mutable Over Immutable

There is no universal rule, but several practical criteria can guide your decision.

Use mutable objects when:

  • The object is updated frequently and the update operation is cheap compared to the cost of creating a new instance.
  • The object is large and copying it would be expensive.
  • You need to pass the object to methods that modify it in place, such as Collections.sort().
  • You are building a data structure incrementally, like a StringBuilder or a HashMap.

Use immutable objects when:

  • The object represents a value that should not change once created, such as a date, a monetary amount, or a configuration.
  • You need to share the object across threads without synchronization.
  • You want to use the object as a key in a HashMap or HashSet, because its hash code will never change.
  • You want to avoid defensive copies when passing the object to other code.

A common pattern is to use a mutable builder to construct an immutable object. For example, StringBuilder builds a String, and Stream.Builder builds a Stream. This gives you the efficiency of mutable construction and the safety of immutable results.

Building an Immutable Class: A Practical Example

Let's build a more realistic immutable class that holds a collection. The challenge is that collections are mutable, so you must protect them.

import java.util.ArrayList; import java.util.Collections; import java.util.List; public final class Book { private final String title; private final List<String> authors; public Book(String title, List<String> authors) { this.title = title; this.authors = new ArrayList<>(authors); // defensive copy } public String getTitle() { return title; } public List<String> getAuthors() { return Collections.unmodifiableList(authors); } }

The constructor copies the input list so that changes to the original list do not affect the Book. The getter returns an unmodifiable view, so callers cannot modify the internal list. This is essential because a simple return authors would expose the internal mutable state.

If you need to return a new Book with an added author, you would create a new list and a new Book instance:

public Book withAuthor(String author) { List<String> newAuthors = new ArrayList<>(authors); newAuthors.add(author); return new Book(title, newAuthors); }

This pattern preserves immutability while allowing logical changes.

Common Pitfalls with Mutable Fields in Immutable Classes

The most common mistake is forgetting to defensively copy mutable fields. Even if a field is final, the object it references can still be modified. Arrays, Date, ArrayList, and other mutable types are frequent culprits.

Consider this flawed immutable class:

public final class BadBook { private final String title; private final String[] authors; public BadBook(String title, String[] authors) { this.title = title; this.authors = authors; // dangerous } public String[] getAuthors() { return authors; // exposes internal array } }

Callers can modify the array through the getter, breaking immutability. The fix is to clone the array in the constructor and return a clone in the getter, or use a List with defensive copies as shown earlier.

Another issue is using Date, which is mutable. If you store a Date field, you must copy it in the constructor and return a copy in the getter. The same applies to LocalDate and LocalDateTime, which are immutable and safe to use directly.

Maintainability and API Design

Immutability has a significant impact on how your API is used. Immutable objects are easier to reason about because their state is fixed. This reduces the cognitive load for developers who call your code and makes it easier to test.

On the other hand, immutable objects can lead to a proliferation of small classes and methods that create new instances. This can make the codebase feel verbose, especially if you need many different variations of an object. In such cases, a mutable builder or a fluent API can mitigate the verbosity.

When designing a public API, consider whether the objects you expose should be mutable or immutable. Value objects like String, Integer, and BigDecimal are immutable by design. Data transfer objects (DTOs) that are populated from a database or network request are often mutable because they are assembled incrementally. The choice should be driven by how the object is used, not by a blanket preference.

Ultimately, the java mutable vs immutable decision is about tradeoffs. Immutability gives you thread safety, predictability, and safer sharing. Mutability gives you performance and flexibility for stateful operations. By understanding the mechanics and the practical consequences, you can choose the right approach for each class in your application.

java mutable vs immutable: Practical Usage and Code Examples | RYUSLOG DEV