What is an Immutable Object and Why is it Needed?
Immutable object is an object whose state cannot be changed after construction (creation). Pros:
- Default thread safety (without synchronization).
- Simple invariants: if the object is valid in the constructor, it is valid always.
- Safe sharing/caching (can be freely passed between threads, placed in caches).
- Convenient as keys in
Map/elements inSet(stablehashCode). - Easier to test and reason about.
Rules for Constructing an Immutable Class
- Make the class
final(or all fieldsprivate final+ forbid inheritance “logically”). - All fields are
private final. - Initialization only in the constructor (no setters afterwards).
- Don’t “leak
this” from the constructor (don’t pass a reference to itself outside after initialization). - Defensive copy for:
- Incoming mutable arguments (arrays, collections,
Date, any mutable types); - Returned values (getters should not return “live” internal references).
- Deep immutability: if a field is a collection/array/object, take care of deep protection (see below).
Basic Example (Correct)
import java.util.List;
import java.util.Objects;
public final class Money {
private final String currency; // immutable type (String is immutable)
private final long minorUnits; // cents
private final List<Integer> tags; // NOTE: collection is mutable by nature
public Money(String currency, long minorUnits, List<Integer> tags) {
this.currency = Objects.requireNonNull(currency);
this.minorUnits = minorUnits;
// defensive copy + immutable wrapper
this.tags = List.copyOf(tags); // Java 10+: essentially an unmodifiable snapshot
}
public String currency() { return currency; }
public long minorUnits() { return minorUnits; }
public List<Integer> tags() {
// can return tags directly, because copyOf made an immutable snapshot
return tags;
}
// functional style "with change": create a NEW object with a different value
public Money withMinorUnits(long newMinorUnits) {
return new Money(this.currency, newMinorUnits, this.tags);
}
@Override public boolean equals(Object o) { /* standard on two fields + tags */ }
@Override public int hashCode() { /* consistent with equals */ }
@Override public String toString() { /* convenient for logs */ }
}Explanation:
List.copyOf(...)returns an immutable snapshot of the current content (and throwsNullPointerExceptionfornullelements).- If deep immutability is needed (elements of the collection are themselves mutable), copy the elements (see “depth of immutability”).
Working with Arrays and Collections
Arrays
Arrays are mutable. Therefore:
- In the constructor:
this.arr = Arrays.copyOf(arr, arr.length); - In the getter:
return Arrays.copyOf(arr, arr.length);
Collections
- For shallow immutability:
List.copyOf,Set.copyOf,Map.copyOf(Java 10+) orCollections.unmodifiableXxx(...). unmodifiableListonly prevents mutation through the wrapper, but if you hold a reference to the original mutable collection, the changes are visible. Therefore:- either create a new collection and wrap it,
- or use
copyOf(makes a snapshot).
- For deep immutability: copy the elements (if they are mutable).
Common Mistakes (Interview Loves Them)
- Returning a live reference to an internal collection/array:
- An “immutable” class that returns
getList()and it can be modified – it’s not immutable.
- No defensive copy in the constructor:
- You passed a list, saved the reference as is → the external side will change the list after the constructor – the class “broke”.
- Mutable types inside immutable wrapper:
- Storing
java.util.Date(mutable) without a copy → changes through the reference. Usejava.time(immutable).
- Inheritance from an immutable class:
- A subclass can add mutable state/methods – it breaks the guarantee. Result: make the base class
final(or constructorprivate+ factories).
- Violation of equals/hashCode during “mutation”:
- If the object is a key in
HashMap, itshashCodemust remain stable. Therefore – immutability.
Immutability and record (Java 16+)
record is syntactic sugar for “data”. Example:
public record Point(int x, int y) {}Features:
- All components are
final, getters are generated,equals/hashCode/toStringare already ready. - But: immutability is shallow. If a field is a collection/array/any mutable type, you need a defensive copy in the compact constructor:
public record Team(String name, List<String> members) {
public Team {
name = Objects.requireNonNull(name);
members = List.copyOf(members); // make a snapshot
}
}Immutability and Lombok
@Value→ makes the classfinal, fieldsprivate final, getters,equals/hashCode.- Still manually provide defensive copy for arrays/collections/
Date.
@lombok.Value
public class UserDto {
String name;
List<String> roles; // in the constructor, make List.copyOf(...)
}”Functional Updates”: with* methods
In the immutable model, changes are made by creating a new object:
public Money withCurrency(String newCurrency) {
if (this.currency.equals(newCurrency)) return this; // micro-optimization
return new Money(newCurrency, this.minorUnits, this.tags);
}This is convenient and safe for multithreading.
Serialization of Immutable Classes
- Serialized/deserialized normally (Java/JSON/Protobuf).
- If a singleton is needed – add
readResolve(). - For JSON deserialization (Jackson) use:
- fully-argument constructor or
@JsonCreator+@JsonProperty.
Why String is Immutable in Java
- Security: Strings are often used as identifiers, class names, resource names, ClassLoader parameters, SQL strings, etc. If someone could change a string “in place”, it would open up a massive vulnerability (path substitution, class name substitution, SQL injection at the object level, etc.).
- String Pool (interning):
String literals are stored in a pool: identical literals refer to one object for memory savings and faster
==comparison between literals. The pool is possible only if strings are immutable (otherwise changing a string would change all references to it). - Caching
hashCode:String.hashCode()is calculated once and cached in a field. If a string could be changed, the cache would become invalid → violation ofMap/Setcontracts. - Thread safety and sharing: The same string can be freely shared between threads without synchronization.
- JVM/JIT optimizations: Immutable objects are easier to optimize (escape analysis, constant inlining, constant folding).
Conclusion: immutability is the foundation of String for security, performance, and predictability.
Mini-examples: Correct/Incorrect
❌ Incorrect (leaking internal array)
public final class Bad {
private final int[] data;
public Bad(int[] data) { this.data = data; } // no copy!
public int[] getData() { return data; } // returned a live reference!
}✅ Correct
public final class Good {
private final int[] data;
public Good(int[] data) { this.data = data.clone(); }
public int[] data() { return data.clone(); }
}✅ Correct for dates: use java.time
public final class Booking {
private final java.time.Instant from;
private final java.time.Instant to;
public Booking(Instant from, Instant to) {
this.from = from; this.to = to; // Instant is immutable
}
}Depth of Immutability
If a field is a list of Address objects, and Address is mutable, then:
- Make immutable Address (preferably), or
- Perform a deep copy of the elements and wrap the list in
List.copyOf.
public record Address(String city, String street) {} // immutable
public final class Person {
private final List<Address> addresses;
public Person(List<Address> addresses) {
this.addresses = List.copyOf(addresses); // elements are already immutable
}
public List<Address> addresses() { return addresses; }
}Best Practices (short checklist)
- Make the class
final+ all fieldsprivate final. - No setters. “Changes” are made through
with*methods, creating a new object. - Defensive copy for arrays/collections/mutable types on input and output.
- Use
java.timeinstead ofDate/Calendar. - For collections –
List.copyOf/Map.copyOf(orCollections.unmodifiableXxx+ preliminary copy). - Avoid leaking
thisin the constructor (no callbacks/listener registration there). - Store invariants in one place – in the constructor/factory, check
requireNonNull, ranges, etc. - For performance, you can cache derived values (e.g.,
hashCode) – this is safe if the object is immutable.