How to create an Immutable Class in Java ?

Immutable class is a key concept in Core Java that ensures an object’s state cannot be changed after creation. Lets explore with a practical example with best practices, explain the steps, and highlight why it’s immutable.


What is an Immutable Class?

  • Definition: A class whose instances cannot be modified after construction. Once created, its state (fields) remains constant.
  • Benefits: Thread-safety, simpler reasoning, safe for caching (e.g., in HashMap).
  • Examples in Java: String, Integer, LocalDate.

Rules for Creating an Immutable Class

  1. Make the class final (prevent subclassing).
  2. Declare all fields as private and final (no direct access, no reassignment).
  3. Provide only getters, no setters.
  4. Ensure constructor initializes all fields (deep copy if mutable objects).
  5. Return defensive copies of mutable fields in getters.
  6. Prevent external modification of mutable objects passed to the constructor.

Example: Immutable Person Class:

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public final class Person {
    private final String name;           // Primitive wrapper (immutable by nature)
    private final int age;               // Primitive (immutable)
    private final List hobbies;  // Mutable object (needs protection)

    // Constructor
    public Person(String name, int age, List hobbies) {
        this.name = name;                // Direct assignment (String is immutable)
        this.age = age;                  // Primitive, immutable
        // Defensive copy to prevent external modification
        this.hobbies = new ArrayList<>(hobbies); 
    }

    // Getters (no setters)
    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    // Return a defensive copy or unmodifiable view
    public List getHobbies() {
        return Collections.unmodifiableList(hobbies); // Prevents modification
        // Alternative: return new ArrayList<>(hobbies); // New copy each time
    }

    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + ", hobbies=" + hobbies + "}";
    }
}

Test the Immutability:

import java.util.Arrays;
import java.util.List;

public class ImmutableTest {
    public static void main(String[] args) {
        // Create a mutable list
        List hobbies = Arrays.asList("Reading", "Coding");
        Person person = new Person("Alice", 25, hobbies);

        // Try to modify the original list
        hobbies.set(0, "Gaming"); // Allowed on original list
        System.out.println("Original Person: " + person); // Hobbies unchanged

        // Try to modify the returned list
        List returnedHobbies = person.getHobbies();
        try {
            returnedHobbies.add("Swimming"); // Throws UnsupportedOperationException
        } catch (Exception e) {
            System.out.println("Cannot modify hobbies: " + e);
        }

        System.out.println("Person after attempted change: " + person);
    }
}

Output

Original Person: Person{name='Alice', age=25, hobbies=[Reading, Coding]}
Cannot modify hobbies: java.lang.UnsupportedOperationException
Person after attempted change: Person{name='Alice', age=25, hobbies=[Reading, Coding]}

Why This Class is Immutable:

  1. final Class: public final class Person prevents subclassing, which could override methods to mutate state.
  2. Private Final Fields: name, age, and hobbies are private (no external access) and final (no reassignment after constructor).
  3. No Setters: Only getters exist, preventing field updates.
  4. Defensive Copy in Constructor: new ArrayList<>(hobbies) ensures the internal hobbies list isn’t tied to the input list.
  5. Defensive Copy in Getter: Collections.unmodifiableList(hobbies) returns a read-only view, blocking modifications to the internal list.

Notes:

  • Mutable Fields: If hobbies weren’t copied defensively, external code could modify the original list, breaking immutability:java// Without defensive copy this.hobbies = hobbies; // Bad—external changes affect Person
  • Deep Copy: For nested mutable objects (e.g., a List<Person>), ensure deep copying if needed.
  • Performance: Returning a new copy in getHobbies() (vs. unmodifiable view) increases memory usage but ensures isolation.
  • Why Immutable: String and int are inherently immutable, so no extra copying is needed.

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *