Java - Mutable vs Immutable Objects
Interview Deep Dive
"If you can't explain immutability under pressure, you don't understand it well enough."
Mutability is one of the most deceptively simple topics in Java. It touches memory layout, thread safety, API design, security, and GC performance. This article covers 25+ curated interview questions — each with working code, detailed explanations, and the reasoning interviewers are looking for.
Core Concepts
What Makes an Object Mutable or Immutable?
The Immutability Checklist
Mutable vs Immutable — At a Glance
| Aspect | Mutable | Immutable |
|---|---|---|
| State after creation | Can change | Never changes |
| Thread safety | Requires synchronization | Inherently thread-safe |
| HashMap key safety | ⚠️ Dangerous | ✅ Safe |
| Memory pattern | Modify in-place | Create new copies |
| JDK examples | StringBuilder, ArrayList, Date |
String, Integer, LocalDate |
| Caching | Risky (shared state) | Safe (can freely share) |
| GC pressure | Lower (reuse) | Higher (new objects per change) |
Beginner Level
Q1: What is a mutable object? What is an immutable object?
A: A mutable object can have its state changed after construction. An immutable object's state is fixed at creation — any "modification" returns a new object.
// MUTABLE — state changes in-place
StringBuilder sb = new StringBuilder("hello");
sb.append(" world"); // sb itself is modified
System.out.println(sb); // "hello world"
// IMMUTABLE — original is untouched, new object created
String s = "hello";
String s2 = s.concat(" world"); // Returns NEW String
System.out.println(s); // "hello" — unchanged!
System.out.println(s2); // "hello world"
Q2: Name all immutable classes in the JDK.
A: The main ones:
| Category | Immutable Classes |
|---|---|
| Wrappers | Integer, Long, Double, Float, Short, Byte, Character, Boolean |
| Strings | String |
| Math | BigInteger, BigDecimal |
| Time (java.time) | LocalDate, LocalTime, LocalDateTime, Instant, Duration, Period, ZonedDateTime |
| Collections | List.of(), Set.of(), Map.of() return unmodifiable collections |
| Other | Optional, URI, URL, UUID, Pattern, InetAddress |
| Records | All record classes (Java 16+) |
Q3: Why is String immutable in Java?
A: Three critical reasons:
String Pool / Interning — The JVM deduplicates identical strings. If strings were mutable, changing one would corrupt all references sharing the same pooled instance.
Security — Strings are used for class loading, file paths, network connections, and credentials. Immutability prevents tampering after validation.
Thread safety — Strings can be freely shared across threads without synchronization.
hashCode caching —
Stringcaches itshashCodeon first computation. This is safe only because the content never changes.
String a = "hello";
String b = "hello";
System.out.println(a == b); // true — same pooled instance
// If String were mutable:
// a.setChar(0, 'H'); // Would corrupt b as well!
Q4: What is the difference between String, StringBuilder, and StringBuffer?
A:
| Feature | String |
StringBuilder |
StringBuffer |
|---|---|---|---|
| Mutability | Immutable | Mutable | Mutable |
| Thread-safe | Yes (immutable) | No | Yes (synchronized) |
| Performance | Slow for concat | Fast | Slower than StringBuilder |
| Since | JDK 1.0 | JDK 1.5 | JDK 1.0 |
// ❌ Creates ~1000 intermediate String objects
String result = "";
for (int i = 0; i < 1000; i++) {
result += i; // Each += creates a NEW String
}
// ✅ Mutates a single buffer — O(n) vs O(n²)
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append(i);
}
String result = sb.toString();
Q5: How do you create an immutable class in Java?
A: Follow all five rules:
// 1. Class is final — prevents subclassing
public final class Money {
// 2. All fields are private and final
private final BigDecimal amount;
private final Currency currency;
// 3. Constructor performs defensive copy of mutable inputs
public Money(BigDecimal amount, Currency currency) {
this.amount = amount; // BigDecimal is already immutable
this.currency = currency; // Currency is already immutable
}
// 4. No setters — only getters
public BigDecimal getAmount() { return amount; }
public Currency getCurrency() { return currency; }
// 5. "Modification" returns a new object
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Currency mismatch");
}
return new Money(this.amount.add(other.amount), this.currency);
}
}
Intermediate Level
Q6: What is a defensive copy? When is it needed?
A: A defensive copy creates a new independent copy of a mutable object to prevent external code from modifying an immutable object's internal state.
// ❌ BROKEN immutability — Date is mutable!
public final class Event {
private final Date startDate;
public Event(Date startDate) {
this.startDate = startDate; // Stores the SAME reference
}
public Date getStartDate() {
return startDate; // Leaks the SAME reference
}
}
// Attack:
Date date = new Date();
Event event = new Event(date);
date.setYear(2099); // Mutates event's internal state!
event.getStartDate().setYear(1900); // Also mutates it!
// ✅ CORRECT — defensive copies on BOTH sides
public final class Event {
private final Date startDate;
public Event(Date startDate) {
this.startDate = new Date(startDate.getTime()); // Copy IN
}
public Date getStartDate() {
return new Date(startDate.getTime()); // Copy OUT
}
}
Key rule: Defensive copy in the constructor, defensive copy in the getter. Miss either and immutability is broken.
Q7: Are Java Records truly immutable?
A: Records are shallowly immutable — their fields are final, but if a field references a mutable object, that object can still be modified.
// Shallowly immutable — the List field is mutable!
record Team(String name, List<String> members) {}
Team team = new Team("Alpha", new ArrayList<>(List.of("Alice", "Bob")));
team.members().add("Charlie"); // ⚠️ Mutates the record's data!
System.out.println(team.members()); // [Alice, Bob, Charlie]
// ✅ Fix: defensive copy in compact constructor
record Team(String name, List<String> members) {
Team {
members = List.copyOf(members); // Unmodifiable copy
}
}
Q8: What happens when you use a mutable object as a HashMap key?
A: The HashMap breaks. When the key's hashCode changes after insertion, the entry becomes unretrievable — it's in the wrong bucket.
public class MutableKey {
private String name;
public MutableKey(String name) { this.name = name; }
public void setName(String name) { this.name = name; }
@Override
public int hashCode() { return name.hashCode(); }
@Override
public boolean equals(Object o) {
return o instanceof MutableKey mk && name.equals(mk.name);
}
}
Map<MutableKey, String> map = new HashMap<>();
MutableKey key = new MutableKey("alice");
map.put(key, "value");
System.out.println(map.get(key)); // "value" ✅
key.setName("bob"); // ⚠️ Mutate the key!
System.out.println(map.get(key)); // null ❌ — lost!
System.out.println(map.size()); // 1 — entry exists but unreachable
// Even creating a NEW key with the original value doesn't help:
System.out.println(map.get(new MutableKey("alice"))); // null ❌
// The entry is in "alice"'s bucket, but equals() now checks "bob"
Rule: Always use immutable objects as HashMap/HashSet keys.
Q9: How does immutability relate to thread safety?
A: Immutable objects are inherently thread-safe because there's no shared mutable state to synchronize.
// MUTABLE — needs synchronization
class MutableCounter {
private int count = 0;
// Without synchronized, this is a race condition
public synchronized void increment() { count++; }
public synchronized int getCount() { return count; }
}
// IMMUTABLE — no synchronization needed
final class ImmutableCounter {
private final int count;
public ImmutableCounter(int count) { this.count = count; }
public int getCount() { return count; }
public ImmutableCounter increment() {
return new ImmutableCounter(count + 1); // New object
}
}
// Used across threads without locks:
// AtomicReference<ImmutableCounter> ref = new AtomicReference<>(new ImmutableCounter(0));
// ref.updateAndGet(ImmutableCounter::increment);
Q10: What is the String interning pool? How does immutability enable it?
A: The JVM maintains a pool of unique String instances. Identical string literals share the same object. This is only safe because String is immutable.
String a = "hello"; // Goes to pool
String b = "hello"; // Reuses same pooled instance
String c = new String("hello"); // Separate heap object
String d = c.intern(); // Returns pooled instance
System.out.println(a == b); // true — same pool entry
System.out.println(a == c); // false — c is on heap
System.out.println(a == d); // true — intern() returns pool entry
Q11: What is the difference between unmodifiable and immutable collections?
A: An unmodifiable wrapper prevents modification through that reference, but the underlying collection can still change. A truly immutable collection can never change.
// UNMODIFIABLE — wrapper, not a copy
List<String> original = new ArrayList<>(List.of("a", "b"));
List<String> unmodifiable = Collections.unmodifiableList(original);
// unmodifiable.add("c"); // ❌ UnsupportedOperationException
original.add("c"); // ✅ Modifies the original
System.out.println(unmodifiable); // [a, b, c] — ⚠️ changed!
// IMMUTABLE — true copy, no back-door
List<String> immutable = List.copyOf(original);
original.add("d");
System.out.println(immutable); // [a, b, c] — unchanged ✅
| Method | Creates copy? | Truly immutable? |
|---|---|---|
Collections.unmodifiableList() |
❌ | ❌ |
List.of() |
N/A (factory) | ✅ |
List.copyOf() |
✅ | ✅ |
Advanced Level
Q12: Explain the final keyword's role in immutability. Does final make an object immutable?
A: No. final on a variable means the reference cannot be reassigned. The object it points to can still be mutated.
final List<String> list = new ArrayList<>();
// list = new ArrayList<>(); // ❌ Can't reassign reference
list.add("hello"); // ✅ Can mutate the object!
final StringBuilder sb = new StringBuilder("hello");
sb.append(" world"); // ✅ Object mutated through final reference
final is necessary but not sufficient for immutability:
finalfields → prevents reassignment (needed)finalclass → prevents subclassing (needed)But you still need: no setters, defensive copies, and no mutable field leaks
Q13: Can you break immutability using reflection?
A: Yes. Reflection can bypass final field protections:
String s = "hello";
Field valueField = String.class.getDeclaredField("value");
valueField.setAccessible(true);
valueField.set(s, "HACKED".getBytes());
System.out.println(s); // "HACKED" — immutability broken!
// Even worse — this corrupts the string pool:
String pooled = "hello";
System.out.println(pooled); // "HACKED" — all references affected!
Note: Java 9+ module system (
--illegal-access=deny) and Java 17+ strong encapsulation make this harder but not impossible with--add-opens.
Q14: What is the Flyweight pattern and how does immutability enable it?
A: The Flyweight pattern shares immutable objects to reduce memory. Java uses this for Integer caching (-128 to 127) and Boolean.TRUE/Boolean.FALSE.
Integer a = 127;
Integer b = 127;
System.out.println(a == b); // true — cached (same object)
Integer c = 128;
Integer d = 128;
System.out.println(c == d); // false — outside cache range
// Boolean — always cached
Boolean t1 = Boolean.valueOf(true);
Boolean t2 = Boolean.valueOf(true);
System.out.println(t1 == t2); // true — same singleton
This caching is only safe because Integer and Boolean are immutable.
Q15: How does immutability affect garbage collection?
A: Immutability creates a trade-off:
Pros for GC:
Short-lived immutable objects (young gen) are collected cheaply
No need to track mutations (write barriers are simpler)
Safe for string deduplication (
-XX:+UseStringDeduplication)
Cons for GC:
Each "modification" creates a new object → higher allocation rate
More GC pressure under high-throughput mutation workloads
// ❌ High GC pressure — creates 10,000 String objects
String result = "";
for (int i = 0; i < 10_000; i++) {
result = result + i; // New String each iteration
}
// ✅ Low GC pressure — one mutable buffer
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10_000; i++) {
sb.append(i); // Mutates in-place
}
Q16: What is a "effectively immutable" object? How does the JMM treat it?
A: An object whose fields are never modified after construction (even if not declared final) is "effectively immutable." The Java Memory Model guarantees safe publication only for truly immutable objects (all final fields). Effectively immutable objects need explicit safe publication.
// Truly immutable — JMM guarantees safe publication
final class SafePoint {
private final int x, y;
SafePoint(int x, int y) { this.x = x; this.y = y; }
}
// Effectively immutable — NOT guaranteed safe without volatile/sync
class UnsafePoint {
private int x, y; // Not final!
UnsafePoint(int x, int y) { this.x = x; this.y = y; }
// Never modified after construction, but JMM doesn't know that
}
// Safe publication for effectively immutable objects:
volatile UnsafePoint point = new UnsafePoint(1, 2); // ✅ volatile
Q17: Design an immutable class with a mutable collection field.
A:
public final class StudentRoster {
private final String className;
private final List<String> students;
private final Map<String, Integer> grades;
public StudentRoster(String className, List<String> students,
Map<String, Integer> grades) {
this.className = className;
// Defensive copy: create independent unmodifiable copies
this.students = List.copyOf(students);
this.grades = Map.copyOf(grades);
}
public String getClassName() { return className; }
public List<String> getStudents() { return students; } // Already unmodifiable
public Map<String, Integer> getGrades() { return grades; } // Already unmodifiable
// "Modification" returns new instance
public StudentRoster addStudent(String name, int grade) {
var newStudents = new ArrayList<>(students);
newStudents.add(name);
var newGrades = new HashMap<>(grades);
newGrades.put(name, grade);
return new StudentRoster(className, newStudents, newGrades);
}
}
Brain Teasers & Trick Questions
Q18: What is the output of this code?
String s1 = "hello";
String s2 = "hel" + "lo";
String s3 = "hel";
String s4 = s3 + "lo";
System.out.println(s1 == s2);
System.out.println(s1 == s4);
System.out.println(s1 == s4.intern());
A:
true — compile-time constant folding: "hel" + "lo" → "hello" (same pool entry)
false — s3 is a variable, so s3 + "lo" uses StringBuilder at runtime → new heap object
true — intern() returns the pool entry
Q19: Is BigDecimal truly immutable? What about this code?
BigDecimal bd = new BigDecimal("1.0");
BigDecimal bd2 = bd.setScale(2);
System.out.println(bd);
System.out.println(bd2);
System.out.println(bd == bd2);
A: Yes, BigDecimal is immutable. setScale() returns a new BigDecimal:
1.0
1.00
false
The method name setScale is misleading — it doesn't mutate; it returns a new instance. This is a common interview trick.
Q20: Can an immutable object contain a reference to a mutable object and still be safe?
A: Only if the mutable object is never exposed and never modified after construction.
// ❌ UNSAFE — leaks the mutable Date
public final class Broken {
private final Date created;
public Broken() { this.created = new Date(); }
public Date getCreated() { return created; } // Leaks!
}
// ✅ SAFE — uses immutable java.time instead
public final class Fixed {
private final Instant created;
public Fixed() { this.created = Instant.now(); }
public Instant getCreated() { return created; } // Instant is immutable
}
Best practice: Prefer immutable field types (
InstantoverDate,List.of()overArrayList).
Q21: What happens here? Does the array make this class mutable?
public final class Scores {
private final int[] values;
public Scores(int[] values) {
this.values = values.clone(); // Defensive copy ✅
}
public int[] getValues() {
return values; // ⚠️ Is this safe?
}
}
A: No! The getter leaks the internal array. Arrays are always mutable in Java.
Scores s = new Scores(new int[]{90, 85, 95});
int[] leaked = s.getValues();
leaked[0] = 0; // Mutates internal state!
System.out.println(s.getValues()[0]); // 0 — broken!
Fix: Return a defensive copy:
public int[] getValues() {
return values.clone(); // Defensive copy OUT
}
Q22: What is the output?
String s = "Java";
s.concat(" Rocks");
s.toUpperCase();
s.replace('a', 'o');
System.out.println(s);
A: Output is Java. Every method on String returns a new String — the original is never modified. The return values are discarded.
This is one of the most common interview tricks. Many candidates expect "JOVO Rocks".
Q23: Why does this immutable class break in a multi-threaded context?
public final class ImmutableHolder {
private final int x;
private final int y;
public ImmutableHolder(int x, int y) {
this.x = x;
// Leaking 'this' before construction completes:
SomeRegistry.register(this);
this.y = y;
}
}
A: The constructor leaks this before y is assigned. Another thread accessing the registered object may see y = 0 (default value) instead of the intended value. This violates the JMM's final field semantics — the guarantee only holds if this doesn't escape during construction.
Rule: Never let
thisescape from a constructor.
Code Analysis Questions
Q24: Design a thread-safe mutable counter vs an immutable counter. Which is better when?
// Approach 1: Mutable + synchronized
class SyncCounter {
private int count = 0;
public synchronized void increment() { count++; }
public synchronized int get() { return count; }
}
// Approach 2: Mutable + atomic
class AtomicCounter {
private final AtomicInteger count = new AtomicInteger(0);
public void increment() { count.incrementAndGet(); }
public int get() { return count.get(); }
}
// Approach 3: Immutable + CAS
final class ImmutableState {
final int count;
final String label;
ImmutableState(int count, String label) {
this.count = count;
this.label = label;
}
}
class StateHolder {
private final AtomicReference<ImmutableState> state =
new AtomicReference<>(new ImmutableState(0, "init"));
public void increment() {
state.updateAndGet(s -> new ImmutableState(s.count + 1, s.label));
}
}
| Approach | Best When |
|---|---|
synchronized |
Simple, low contention |
AtomicInteger |
Single field, high contention |
Immutable + AtomicReference |
Multiple fields that must change atomically |
Q25: What is the copy-on-write pattern?
A: A hybrid approach: shared immutable state for reads, copy + mutate for writes. Used by CopyOnWriteArrayList.
// Simplified copy-on-write pattern
public class CowList<E> {
private volatile Object[] elements = new Object[0];
public E get(int index) {
return (E) elements[index]; // Lock-free read
}
public synchronized void add(E element) {
Object[] newArray = Arrays.copyOf(elements, elements.length + 1);
newArray[elements.length] = element;
elements = newArray; // Atomic swap via volatile
}
}