Immutable class in java
Published on August 14, 2024
Building Immutable Objects in Java: A Deep Dive with Examples
Creating immutable objects in Java ensures that once an object is created, its state cannot be changed. This immutability is particularly useful in multi-threaded applications and helps in building more predictable and thread-safe systems. In this post, we’ll walk through the essential steps to design an immutable class, followed by an example.
Key Steps to Creating an Immutable Class
-
Declare the Class as
final
:- This prevents subclassing, which could otherwise alter the behavior of the immutable class.
-
Make All Properties
private
andfinal
:- Declare all fields as
private
to ensure they are not accessible from outside the class, andfinal
to ensure they can only be assigned once.
- Declare all fields as
-
Do Not Declare Setters:
- Setters allow modification of fields, which contradicts immutability. Only provide getter methods to access the field values.
-
Declare a Constructor with All Arguments:
- This constructor should initialize all the fields of the class. Since the fields are
final
, they must be set during object construction.
- This constructor should initialize all the fields of the class. Since the fields are
-
Implement Clone Method for Custom Nested Objects:
- If your class contains nested objects that are mutable, ensure they implement
Cloneable
and provide aclone
method to return a copy of these objects.
- If your class contains nested objects that are mutable, ensure they implement
-
Perform Deep Copy for Other Nested Objects:
- For collections or other mutable objects, create copies of these objects to prevent external modifications from affecting the immutable object.
The ImmutableEmployee Class: An Example
Here’s how you can apply these steps to create an immutable Employee
class:
package ch.souradip;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
final class ImmutableEmployee {
private final String empName;
private final int age;
private final Address address;
private final List<String> phoneNumbers;
private final Map<String, String> metadata;
public ImmutableEmployee(String name, int age, Address address, List<String> phoneNumbers, Map<String, String> metadata) {
this.empName = name;
this.age = age;
this.address = new Address(address.getStreet(), address.getCity()); // Clone or copy nested object
this.phoneNumbers = new ArrayList<>(phoneNumbers); // Deep copy for collection
this.metadata = new HashMap<>(metadata); // Deep copy for map
}
public String getEmpName() {
return empName;
}
public int getAge() {
return age;
}
// Clone the address object to ensure immutability
public Address getAddress() throws CloneNotSupportedException {
return (Address) address.clone();
}
// Deep copy the list of phone numbers
public List<String> getPhoneNumbers() {
return new ArrayList<>(phoneNumbers);
}
// Deep copy the map of metadata
public Map<String, String> getMetadata() {
return new HashMap<>(metadata);
}
}
Address Class: Custom Nested Object
Here’s how the Address
class is implemented:
final class Address implements Cloneable {
private String street;
private String city;
public Address(String street, String city) {
this.street = street;
this.city = city;
}
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
@Override
public String toString() {
return "{Street: " + street + ", City: " + city + "}";
}
}
Example Usage
Let’s see how the ImmutableEmployee
class behaves when trying to modify its fields:
public class ImmutableClassExample {
public static void main(String[] args) throws CloneNotSupportedException {
Address address1 = new Address("s1", "c1");
List<String> phoneNumbers = new ArrayList<>();
phoneNumbers.add("123345");
phoneNumbers.add("456789");
Map<String, String> metadata = new HashMap<>();
metadata.put("hobby", "Watching Movies");
ImmutableEmployee e = new ImmutableEmployee("John", 23, address1, phoneNumbers, metadata);
// Attempted modifications
e.getAddress().setCity("c3");
e.getAddress().setStreet("s3");
e.getPhoneNumbers().add("1234");
e.getMetadata().put("skill", "Java");
System.out.println(e.getEmpName());
System.out.println(e.getAge());
System.out.println(e.getAddress());
System.out.println(e.getPhoneNumbers());
System.out.println(e.getMetadata());
}
}
Output and Explanation
John
23
{Street: s1, City: c1}
[123345, 456789]
{hobby=Watching Movies}
Despite attempts to modify the fields of ImmutableEmployee
, the original state remains unchanged. This is because:
- The
Address
object is cloned, so changes to the cloned address do not affect the originaladdress
. - The
phoneNumbers
list andmetadata
map are deep-copied, ensuring that external modifications do not alter the internal state.
Conclusion
Designing immutable classes in Java involves ensuring that the object's state cannot change once it’s created. By following the steps outlined—making the class final
, using final
fields, avoiding setters, and implementing deep copies—you can create robust, thread-safe objects. The ImmutableEmployee
example demonstrates these principles in action, providing a clear model for creating immutable objects in your own applications.