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

  1. Declare the Class as final:

    • This prevents subclassing, which could otherwise alter the behavior of the immutable class.
  2. Make All Properties private and final:

    • Declare all fields as private to ensure they are not accessible from outside the class, and final to ensure they can only be assigned once.
  3. Do Not Declare Setters:

    • Setters allow modification of fields, which contradicts immutability. Only provide getter methods to access the field values.
  4. 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.
  5. Implement Clone Method for Custom Nested Objects:

    • If your class contains nested objects that are mutable, ensure they implement Cloneable and provide a clone method to return a copy of these objects.
  6. 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 original address.
  • The phoneNumbers list and metadata 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.