Question 8 · Section 13

Is it enough to make all fields final for immutability?

If the class consists only of primitives (int, double, boolean) and immutable types (String, BigDecimal, LocalDate), then final for all fields + final for the class — is suffici...

Language versions: English Russian Ukrainian

Basic Level

No, it’s not enough! final only protects the variable itself (the reference), not the data it points to.

Example of the problem

public final class NotReallyImmutable {
    private final List<String> items; // field is final

    public NotReallyImmutable(List<String> items) {
        this.items = items; // saved a direct reference!
    }

    public List<String> getItems() {
        return items; // returned a direct reference!
    }
}
List<String> myItems = new ArrayList<>();
myItems.add("A");
NotReallyImmutable obj = new NotReallyImmutable(myItems);

myItems.add("B"); // Changed the "immutable" object from outside!
// this.items = items saves the same reference as myItems.
// Therefore myItems.add("B") changes the list inside the object.
obj.getItems().clear(); // Can also be changed through the getter!

What else is needed

  1. The class must be final
  2. Make copies of collections in the constructor
  3. Return copies (or immutable wrappers) from getters

final fields vs immutable class

  final fields Immutable class
Fields cannot be reassigned Yes Yes
Mutable fields protected No Yes (defensive copy)
Inheritance prohibited No Yes (final class)
Getters return copies No Yes

Intermediate Level

Shallow Immutability vs Deep Immutability

Shallowfinal fields, but contents of mutable objects can be changed:

private final List<User> users; // the list cannot be replaced, but elements can be changed

Deep — everything, including nested objects, is immutable:

public final class Group {
    private final List<String> names; // String is immutable — deep protection ensured

    public Group(List<String> names) {
        this.names = List.copyOf(names); // copy
    }

    public List<String> getNames() {
        return names; // List.copyOf already returned an immutable list
    }
}

When final is sufficient

If the class consists only of primitives (int, double, boolean) and immutable types (String, BigDecimal, LocalDate), then final for all fields + final for the class — is sufficient.

When final is NOT sufficient

  • Arrays — elements can be changed
  • Collections (List, Map, Set) — contents can be changed
  • Date — has setTime(), setYear() methods, etc.
  • Custom mutable classes

Advanced Level

Reflection Attack

Even a “perfectly” immutable class can be attempted to be changed via Reflection:

Field field = MyClass.class.getDeclaredField("value");
field.setAccessible(true);
field.set(obj, newValue); // breaks immutability

Starting with Java 9 (Project Jigsaw), the module system limits reflection, and with Java 16 (JEP 396), access to java.base fields is blocked by default.

The complete formula for immutability

final class + final fields + defensive copies + no this-escape = True Immutability

Summary for Advanced

  • final only protects the reference, not the data at the reference
  • For collections and arrays, defensive copying is mandatory
  • Immutability is a property of the entire object hierarchy, not just the top level
  • Remember: final fields + final class + defensive copies = Immutable
  • In Java 14+, use record — it applies most rules automatically

Interview Cheat Sheet

Must know:

  • final only protects the reference, not the data at the reference
  • Shallow Immutability — final fields, but contents of mutable objects can be changed
  • Deep Immutability — everything, including nested objects, is immutable
  • Formula: final class + final fields + defensive copies + no this-escape = True Immutability
  • When final is sufficient: primitives + immutable types (String, BigDecimal, LocalDate)
  • When final is NOT sufficient: arrays, collections, Date, custom mutable classes
  • Reflection Attack: even a perfectly immutable class can be changed via reflection (before Java 16)

Frequent follow-up questions:

  • Why doesn’t final List protect?list.add() changes the contents, the reference is the same
  • When is final sufficient? — Only if the class consists of primitives and immutable types
  • What is Shallow vs Deep Immutability? — Shallow: container is unchanged, elements — not; Deep: everything is unchanged
  • Can reflection break immutability? — Yes, but in Java 16+ access to java.base fields is blocked

Red flags (DO NOT say):

  • “Final fields = immutable class” — without defensive copy and final class, not enough
  • “List.copyOf not needed if field is final” — final protects the reference, not the contents
  • “clone() is the best way to copy” — clone() is considered broken (Effective Java Item 13)
  • “Reflection — not a problem” — before Java 16 this is a real threat

Related topics:

  • [[1. What is an immutable object]]
  • [[7. What is the final keyword and how does it help create immutable classes]]
  • [[9. What to do if a class field references a mutable object]]
  • [[10. What is a defensive copy]]
  • [[14. What is the difference between shallow copy and deep copy]]