Back to Blog
Java

Java Immutable Class: Design and Implementation

java immutable class: Learn how to design immutable classes in Java: final fields, no setters, defensive copying, and why they simplify concurrency and caching.

immutable objectsJavaconcurrencyvalue objectsdefensive copying
A Java immutable class diagram showing final fields and no setters, with a lock icon representing thread safety.

java immutable class requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

In Java, an immutable class is one whose instances cannot be modified after they are created. This property is enforced by the class design: all fields are final, no setter methods exist, and any reference to a mutable object is either not exposed or is defensively copied. Immutable classes are a core building block for reliable concurrent code, because they can be shared freely between threads without synchronization. This article walks through the exact requirements, a complete implementation, the common pitfalls, and the practical tradeoffs you need to consider when designing a Java immutable class.

Core Requirements for an Immutable Class

To make a class immutable in Java, you need to satisfy several structural rules. The most obvious one is that every field must be declared final. That prevents reassignment of the field after the constructor finishes. But final alone is not enough. You also need to ensure that no method can modify the state of the object. That means you cannot provide setter methods, and any method that might otherwise mutate a field must be omitted or redesigned to return a new instance instead.

Another requirement is that the class itself should not be extensible. If a subclass can add mutable state or override methods, immutability is lost. Declare the class final or use a private constructor with a static factory method to prevent subclassing. This is a common detail that developers overlook when they first build an immutable class.

Finally, you must handle mutable fields carefully. If a field references an array, a java.util.Date, a List, or any other mutable object, you need to ensure that the original reference never escapes. The constructor should copy the incoming mutable object, and any getter should return a copy rather than the internal reference. This technique is called defensive copying.

A Minimal Immutable Class Example

Here is a simple immutable class that represents a point in two-dimensional space:

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; } }

The class is declared final, so it cannot be subclassbed. The fields x and y are final and assigned only in the constructor. There are no setters, and the getters return primitive values, which are always safe to expose. This class is immutable because there is no way to change x or y after construction.

This minimal example works because the fields are primitives. When a field is a reference to a mutable object, the implementation becomes more involved.

Handling Mutable Fields with Defensive Copying

Consider a class that holds a java.util.Date. The Date class is mutable, so you cannot simply store the reference passed into the constructor. If you do, the caller can change the Date after the object is created, breaking immutability. The same problem applies to arrays and collections.

The solution is to copy the mutable object when it enters the class, and to copy it again when it leaves. Here is an example:

import java.util.Date; public final class Event { private final String name; private final Date timestamp; public Event(String name, Date timestamp) { this.name = name; this.timestamp = new Date(timestamp.getTime()); } public Date getTimestamp() { return new Date(timestamp.getTime()); } }

The constructor creates a new Date from the passed-in object, so the original reference is not stored. The getter returns a new Date as well, so the internal field is never exposed. This pattern ensures that the Event object remains immutable even though it wraps a mutable type.

For collections, the same principle applies. If you store a List<String>, you should copy it into an unmodifiable list in the constructor, and return an unmodifiable view or a copy from the getter. Using Collections.unmodifiableList is a common approach, but you must still copy the incoming list to prevent the caller from mutating the original collection after construction.

Why Immutable Classes Simplify Concurrency

The biggest practical benefit of a Java immutable class is thread safety. Because the state of an immutable object never changes, it can be safely shared across threads without locks, volatile fields, or other synchronization mechanisms. This eliminates a whole class of race conditions and makes concurrent code easier to reason about.

In a multithreaded application, immutable objects are often used as configuration values, request parameters, or cached results. They can be published to multiple threads simply by passing a reference, because no thread can corrupt the state. This is a direct consequence of the Java memory model: final fields have special initialization guarantees, so any thread that sees a reference to an immutable object is guaranteed to see its fully constructed state, as long as the object was safely published.

This does not mean that every object in a concurrent system should be immutable. Mutable objects are still necessary for stateful components like caches, buffers, and accumulators. But for data that is shared read-only, immutability removes a significant amount of complexity.

Performance and Memory Considerations

Immutable classes are not always the most efficient choice in terms of memory allocation. Because every change requires creating a new instance, an application that performs frequent modifications may generate a large number of short-lived objects. This can increase garbage collection pressure. For example, a String concatenation in a loop creates many intermediate immutable String objects, which is why StringBuilder exists.

On the other hand, immutable objects enable certain optimizations that are impossible with mutable ones. They can be cached and reused safely, because no one can alter them. They can be used as keys in hash-based collections without the risk of the hash code changing after insertion. They also allow for structural sharing in functional data structures, where a new version of a collection can share most of its internal nodes with the previous version.

The performance tradeoff depends on your usage pattern. If you create an immutable object once and read it many times, the overhead is negligible. If you are constantly updating the object, the cost of allocating new instances may become significant. In those cases, consider whether a mutable builder or a mutable internal representation is more appropriate.

Common Mistakes That Break Immutability

Even experienced developers sometimes create a class that looks immutable but is not. One common mistake is returning a direct reference to a mutable field. If a getter returns the internal Date or List, the caller can modify it, and the "immutable" object changes. Another mistake is storing a mutable object without copying it in the constructor, which allows the original owner to mutate the data after construction.

Another subtle issue is allowing subclassing. If the class is not final, a subclass can add mutable fields or override methods to change behavior. Even if the base class is perfectly immutable, a subclass can break the contract. For this reason, the class itself should be final, or the constructor should be private and all instances created through a static factory method.

A third mistake involves arrays. An array is mutable even if its elements are immutable. If you store an array and return it directly, the caller can change its contents. You need to copy the array in the constructor and return a copy from the getter, or use Arrays.copyOf.

When to Choose an Immutable Class

Immutable classes are the right choice when you need a value object that represents a snapshot of data, such as a monetary amount, a date range, a user profile, or a configuration entry. They are also ideal for objects that will be shared across threads or used as cache keys. The record feature in Java 16+ provides a concise way to declare such classes, but the same principles apply to traditional classes.

Use an immutable class when the object's state is naturally fixed after creation, and when you want to avoid defensive synchronization code. Avoid immutability when the object represents a long-lived, stateful entity that changes frequently, such as a session, a connection, or a mutable accumulator. In those cases, forcing immutability would lead to constant object churn and awkward update patterns.

The decision often comes down to whether you need identity or value semantics. Immutable objects work best when two instances with the same state are considered equal. If you rely on object identity, mutable objects may be simpler to manage. For most domain models, however, immutable value objects reduce bugs and make the code easier to test and maintain.

java immutable class: Practical Usage and Code Examples | RYUSLOG DEV