In Java, an immutable class is a class whose instances (objects) cannot be modified once created. Any modification to an instance of an immutable class results in the creation of a new instance, leaving the original instance unchanged. String is the most common example of an immutable class in Java, but you can create your own immutable classes as well.
Why Use Immutable Classes?
Immutable classes offer several benefits, particularly in multi-threaded environments:
- Thread Safety: Immutable objects are inherently thread-safe because they cannot be modified after creation. Multiple threads can safely access immutable instances without requiring synchronization.
- Security: Immutable objects are less susceptible to accidental or malicious modification.
- Memory Efficiency: Because they do not change, they can be safely shared and reused, saving memory.
- Ease of Use: Their state consistency makes them easier to use and less error-prone.
How to Create an Immutable Class in Java
To create an immutable class, follow these guidelines:
-
Declare the Class as
final
: This prevents other classes from extending it and modifying its behavior. -
Make All Fields
final
andprivate
: This ensures that fields can only be assigned once (during construction) and cannot be accessed directly outside the class. -
Do Not Provide "Setter" Methods: Setters allow fields to be modified, which breaks immutability.
-
Initialize All Fields in the Constructor: All fields should be initialized in the constructor, and once set, they should not be changed.
-
Return Copies of Mutable Fields: If the class has fields that hold references to mutable objects (e.g.,
Date
,List
), return a copy of the mutable object rather than the object itself to prevent external modifications. -
Override
equals()
andhashCode()
: Immutable classes should typically override these methods for correct behavior in collections.
Example: Creating an Immutable Class
Here’s an example of an immutable class representing a Person
with a name and age:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
public final class Person { private final String name; // String is already immutable private final int age; private final java.util.Date birthDate; // Mutable field // Constructor public Person(String name, int age, java.util.Date birthDate) { this.name = name; this.age = age; // Create a defensive copy of the mutable Date object this.birthDate = new java.util.Date(birthDate.getTime()); } // Getter for name public String getName() { return name; } // Getter for age public int getAge() { return age; } // Getter for birthDate public java.util.Date getBirthDate() { // Return a defensive copy of the mutable Date field return new java.util.Date(birthDate.getTime()); } // Override equals() and hashCode() for object equality checks @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; Person person = (Person) obj; return age == person.age && name.equals(person.name) && birthDate.equals(person.birthDate); } @Override public int hashCode() { return Objects.hash(name, age, birthDate); } } |
Explanation of the Example
- Immutable Fields:
name
,age
, andbirthDate
are all private and final. - Defensive Copy: The
birthDate
field is of typeDate
, which is mutable. To prevent external changes tobirthDate
, we create a defensive copy in both the constructor and the getter. - No Setters: There are no setter methods, so once a
Person
object is created, its state cannot be changed. equals()
andhashCode()
Implemented: These methods are overridden to ensure correct equality behavior, especially if instances are used in collections likeHashSet
.
Immutable Class with Collections
If your class contains a collection (e.g., List
, Set
), use defensive copying or an unmodifiable wrapper to maintain immutability.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import java.util.Collections; import java.util.List; public final class ImmutablePersonWithHobbies { private final String name; private final List<String> hobbies; public ImmutablePersonWithHobbies(String name, List<String> hobbies) { this.name = name; // Defensive copy to prevent external modification this.hobbies = Collections.unmodifiableList(new ArrayList<>(hobbies)); } public String getName() { return name; } public List<String> getHobbies() { return hobbies; // Return the unmodifiable list } } |
In this example:
- The
hobbies
field is initialized with an unmodifiable view of the list. This ensures the list cannot be changed externally. Collections.unmodifiableList()
wraps the list so that any attempt to modify it (add/remove elements) will throw anUnsupportedOperationException
.
Summary Checklist for Creating Immutable Classes
- Mark the class as
final
. - Make all fields
final
andprivate
. - Initialize all fields in the constructor.
- Do not provide setters.
- For mutable fields, return defensive copies or unmodifiable views.
- Override
equals()
andhashCode()
if needed.
By following these steps, you can create immutable classes that are safe, thread-friendly, and efficient.