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
- Make the class final (prevent subclassing).
- Declare all fields as private and final (no direct access, no reassignment).
- Provide only getters, no setters.
- Ensure constructor initializes all fields (deep copy if mutable objects).
- Return defensive copies of mutable fields in getters.
- 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:
- final Class: public final class Person prevents subclassing, which could override methods to mutate state.
- Private Final Fields: name, age, and hobbies are private (no external access) and final (no reassignment after constructor).
- No Setters: Only getters exist, preventing field updates.
- Defensive Copy in Constructor: new ArrayList<>(hobbies) ensures the internal hobbies list isn’t tied to the input list.
- 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.