How to Create Immutable Classes in Java

Avatar

By squashlabs, Last Updated: August 16, 2023

How to Create Immutable Classes in Java

Introduction to Immutable Class

An immutable class in Java is a class whose objects cannot be modified after they are created. Once an object of an immutable class is created, its state remains constant throughout its lifetime. This means that the values of the object’s fields cannot be changed once they are set. Immutable classes are often used in scenarios where data integrity and thread safety are critical.

Related Article: How To Parse JSON In Java

Definition of Immutable Class

An immutable class is a class in Java that is designed to have a fixed state. Once an object of an immutable class is created, its internal state cannot be modified. This is achieved by making the class final, making all its fields final, and not providing any setter methods. Immutable classes provide the guarantee that their objects will not change over time, ensuring data consistency and eliminating the need for synchronization in multi-threaded environments.

Characteristics of Immutable Class

Immutable classes possess certain key characteristics that differentiate them from mutable classes:

1. Immutability: Objects of an immutable class cannot be modified once they are created. All fields are final and their values are set during object construction.

2. Thread Safety: Immutable classes are inherently thread-safe since their state cannot be modified. Multiple threads can safely access and use immutable objects without the risk of data corruption or race conditions.

3. Data Integrity: Immutable classes ensure data integrity by guaranteeing that the values of their fields remain constant throughout their lifetime. This prevents unexpected changes that could lead to bugs or inconsistencies.

4. Hashcode Stability: Immutable objects have stable hashcodes, which means that their hashcode remains constant throughout their lifetime. This allows immutable objects to be used as keys in hash-based data structures like HashMaps.

Use Case 1: Using Immutable Class for Thread Safety

One of the primary use cases for immutable classes is to achieve thread safety. Immutable objects can be safely shared between multiple threads without the need for synchronization mechanisms like locks or mutexes. Since immutable objects cannot be modified, they are inherently thread-safe and can be accessed concurrently without the risk of data corruption or race conditions.

Consider the following example of an immutable class representing a point in 2D space:

public final class ImmutablePoint {
    private final int x;
    private final int y;
    
    public ImmutablePoint(int x, int y) {
        this.x = x;
        this.y = y;
    }
    
    public int getX() {
        return x;
    }
    
    public int getY() {
        return y;
    }
}

In this example, the ImmutablePoint class has two final fields x and y, which are set during object construction and cannot be modified thereafter. This ensures that the state of the ImmutablePoint object remains constant, making it safe to share between multiple threads.

Related Article: How To Convert Array To List In Java

Use Case 2: Using Immutable Class for Cache Keys

Immutable classes are often used as keys in cache management systems. Since the state of an immutable object cannot be modified, it can be used as a key in a cache without the risk of data corruption. This is because the key’s value will not change over time, ensuring that the cache lookup remains consistent.

Consider the following example of an immutable class representing a cache key:

public final class CacheKey {
    private final String key;
    
    public CacheKey(String key) {
        this.key = key;
    }
    
    public String getKey() {
        return key;
    }
}

In this example, the CacheKey class has a single final field key, which is set during object construction and cannot be modified thereafter. This ensures that the cache key’s value remains constant, making it suitable for use in cache management systems.

Use Case 3: Using Immutable Class for Map keys and Set elements

Immutable classes are commonly used as keys in Maps and elements in Sets. Since the state of an immutable object cannot be modified, it can be used as a key or element in a Map or Set without the risk of inconsistent behavior. Immutable objects provide stable hashcodes, ensuring that they can be reliably used as keys or elements in hash-based data structures.

Consider the following example of an immutable class representing a person:

public final class Person {
    private final String name;
    private final int age;
    
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    public String getName() {
        return name;
    }
    
    public int getAge() {
        return age;
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
    
    @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 && Objects.equals(name, person.name);
    }
}

In this example, the Person class has two final fields name and age, which are set during object construction and cannot be modified thereafter. The class also overrides the hashCode() and equals() methods to ensure proper behavior when used as a key or element in a Map or Set.

Code Snippet 1: Creating an Immutable Class

To create an immutable class in Java, follow these steps:

1. Make the class final to prevent inheritance.
2. Declare all fields private and final to prevent modification.
3. Do not provide any setter methods.
4. Initialize all fields through a constructor.
5. If the class has mutable references, ensure that they are not modified after object construction.
6. Provide getter methods to access the values of the fields.

Here’s an example of an immutable class representing a book:

public final class Book {
    private final String title;
    private final String author;
    private final int publicationYear;
    
    public Book(String title, String author, int publicationYear) {
        this.title = title;
        this.author = author;
        this.publicationYear = publicationYear;
    }
    
    public String getTitle() {
        return title;
    }
    
    public String getAuthor() {
        return author;
    }
    
    public int getPublicationYear() {
        return publicationYear;
    }
}

In this example, the Book class is declared final to prevent inheritance. The fields title, author, and publicationYear are declared private and final to ensure immutability. The constructor initializes these fields, and getter methods are provided to access their values.

Related Article: How To Iterate Over Entries In A Java Map

Code Snippet 2: Adding fields to an Immutable Class

If you need to add additional fields to an immutable class, you can do so by following these steps:

1. Add the new field(s) to the class.
2. Update the constructor to accept the new field(s) as parameters.
3. Update the constructor body to initialize the new field(s).
4. Update the getter methods to include the new field(s).

Here’s an example of adding a genre field to the Book class:

public final class Book {
    private final String title;
    private final String author;
    private final int publicationYear;
    private final String genre;
    
    public Book(String title, String author, int publicationYear, String genre) {
        this.title = title;
        this.author = author;
        this.publicationYear = publicationYear;
        this.genre = genre;
    }
    
    public String getTitle() {
        return title;
    }
    
    public String getAuthor() {
        return author;
    }
    
    public int getPublicationYear() {
        return publicationYear;
    }
    
    public String getGenre() {
        return genre;
    }
}

In this example, the genre field is added to the Book class along with its getter method. The constructor is updated to accept the genre as a parameter and initialize the field accordingly.

Code Snippet 3: Making Immutable Class with final keyword

One way to create an immutable class in Java is by using the final keyword. By making the class final, it cannot be subclassed, ensuring that its behavior and state cannot be modified.

Here’s an example of an immutable class using the final keyword:

public final class MyImmutableClass {
    private final int value;
    
    public MyImmutableClass(int value) {
        this.value = value;
    }
    
    public int getValue() {
        return value;
    }
}

In this example, the MyImmutableClass is declared final, and the value field is declared final as well. The constructor initializes the value field, and a getter method is provided to access its value.

Code Snippet 4: Making Immutable Class with private constructor

Another way to create an immutable class in Java is by using a private constructor. By making the constructor private, other classes cannot directly instantiate objects of the immutable class, ensuring that the state of the objects cannot be modified.

Here’s an example of an immutable class with a private constructor:

public final class MyImmutableClass {
    private final int value;
    
    private MyImmutableClass(int value) {
        this.value = value;
    }
    
    public int getValue() {
        return value;
    }
    
    public static MyImmutableClass create(int value) {
        return new MyImmutableClass(value);
    }
}

In this example, the MyImmutableClass has a private constructor, preventing direct instantiation. Instead, a static factory method create() is provided to create instances of the class. The value field is declared final, ensuring immutability.

Related Article: How To Split A String In Java

Code Snippet 5: Handling Date fields in Immutable Class

Handling Date fields in an immutable class requires special attention because Date is mutable. To ensure immutability, you should make a defensive copy of the Date object during object construction or return a defensive copy in the getter method.

Here’s an example of an immutable class with a Date field:

import java.util.Date;

public final class MyImmutableClass {
    private final Date date;
    
    public MyImmutableClass(Date date) {
        this.date = new Date(date.getTime());
    }
    
    public Date getDate() {
        return new Date(date.getTime());
    }
}

In this example, the MyImmutableClass has a Date field date. During object construction, a defensive copy of the date object is made using the getTime() method to obtain the underlying long value. In the getter method, a new Date object is created using the long value of the date field, ensuring that the returned Date object is a separate instance.

Best Practice 1: Use final keyword for class

To create an immutable class in Java, it is recommended to use the final keyword for the class declaration. By making the class final, it cannot be subclassed, ensuring that its behavior and state cannot be modified.

Here’s an example of an immutable class with the final keyword:

public final class MyImmutableClass {
    // Fields and methods
}

In this example, the MyImmutableClass is declared final, preventing any subclassing.

Best Practice 2: Make constructor private

To enforce immutability, it is best practice to make the constructor of an immutable class private. By doing so, other classes cannot directly instantiate objects of the immutable class, ensuring that the state of the objects cannot be modified.

Here’s an example of an immutable class with a private constructor:

public final class MyImmutableClass {
    private MyImmutableClass() {
        // Private constructor
    }
    
    // Fields and methods
}

In this example, the MyImmutableClass has a private constructor, preventing direct instantiation.

Related Article: How To Convert Java Objects To JSON With Jackson

Best Practice 3: Don’t provide setter methods

To maintain immutability, it is crucial not to provide any setter methods in an immutable class. By omitting setter methods, the state of the objects cannot be modified once they are created.

Here’s an example of an immutable class without setter methods:

public final class MyImmutableClass {
    private final int value;
    
    public MyImmutableClass(int value) {
        this.value = value;
    }
    
    public int getValue() {
        return value;
    }
    
    // No setter methods
}

In this example, the MyImmutableClass has a single final field value and a getter method. Setter methods are not provided, ensuring that the value of the field cannot be modified.

Best Practice 4: Make all fields final and private

To ensure immutability, it is important to make all fields of an immutable class final and private. By making the fields final, their values cannot be changed once they are set. By making the fields private, their values can only be accessed through getter methods.

Here’s an example of an immutable class with final and private fields:

public final class MyImmutableClass {
    private final int value;
    
    public MyImmutableClass(int value) {
        this.value = value;
    }
    
    public int getValue() {
        return value;
    }
}

In this example, the MyImmutableClass has a single final field value, which is private. The value of the field is set during object construction and can only be accessed through the getter method.

Best Practice 5: Return a new object in getter methods

To ensure immutability, it is recommended to return a new object in getter methods that return mutable objects. By returning a new object, the internal state of the immutable class remains unchanged.

Here’s an example of an immutable class with a getter method returning a new object:

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

public final class MyImmutableClass {
    private final List<String> values;
    
    public MyImmutableClass(List<String> values) {
        this.values = new ArrayList<>(values);
    }
    
    public List<String> getValues() {
        return new ArrayList<>(values);
    }
}

In this example, the MyImmutableClass has a List<String> field values. The getter method getValues() returns a new ArrayList containing the elements of the values field. This ensures that the returned list is a separate instance and cannot be modified externally.

Related Article: Storing Contact Information in Java Data Structures

Real World Example 1: Immutable Class in Financial Systems

Immutable classes are commonly used in financial systems to represent financial transactions, positions, or market data. In such systems, it is crucial to maintain data integrity and prevent unauthorized modification of critical financial information. Immutable classes provide a reliable and secure way to represent financial data, ensuring that it remains consistent and tamper-proof.

Here’s an example of an immutable class representing a financial transaction:

public final class FinancialTransaction {
    private final String transactionId;
    private final String accountId;
    private final BigDecimal amount;
    
    public FinancialTransaction(String transactionId, String accountId, BigDecimal amount) {
        this.transactionId = transactionId;
        this.accountId = accountId;
        this.amount = amount;
    }
    
    public String getTransactionId() {
        return transactionId;
    }
    
    public String getAccountId() {
        return accountId;
    }
    
    public BigDecimal getAmount() {
        return amount;
    }
}

In this example, the FinancialTransaction class is declared final and has three final fields representing the transaction ID, account ID, and amount. The constructor initializes these fields, and getter methods are provided to access their values.

Real World Example 2: Immutable Class in Multi-threaded Applications

Immutable classes are particularly useful in multi-threaded applications where concurrent access to shared data is a concern. By ensuring that objects are immutable, thread safety is guaranteed without the need for additional synchronization mechanisms. Immutable classes provide a simple and efficient way to share data between multiple threads without the risk of data corruption or race conditions.

Here’s an example of an immutable class representing a shared configuration:

public final class Configuration {
    private final Map<String, String> properties;
    
    public Configuration(Map<String, String> properties) {
        this.properties = Collections.unmodifiableMap(new HashMap<>(properties));
    }
    
    public String getProperty(String key) {
        return properties.get(key);
    }
}

In this example, the Configuration class has a Map<String, String> field properties. The constructor makes a defensive copy of the properties map using the HashMap copy constructor and wraps it with an unmodifiable map using Collections.unmodifiableMap(). This ensures that the properties map cannot be modified after object construction.

Real World Example 3: Immutable Class in Cache Management Systems

Immutable classes are commonly used in cache management systems to represent cache keys or cache values. By using immutable classes, cache entries can be safely stored and retrieved without the risk of data corruption or unexpected changes. Immutable classes provide stable hashcodes, ensuring efficient lookup and retrieval from hash-based data structures.

Here’s an example of an immutable class representing a cache key:

public final class CacheKey {
    private final String key;
    
    public CacheKey(String key) {
        this.key = key;
    }
    
    public String getKey() {
        return key;
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(key);
    }
    
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        
        CacheKey cacheKey = (CacheKey) obj;
        return Objects.equals(key, cacheKey.key);
    }
}

In this example, the CacheKey class has a single final field key, which is set during object construction and cannot be modified thereafter. The class overrides the hashCode() and equals() methods to ensure proper behavior when used as a key in a cache.

Related Article: How to Convert JSON String to Java Object

Performance Consideration 1: Creation Cost of Immutable Objects

Creating immutable objects may incur a higher initial cost compared to mutable objects due to the need for defensive copying or additional object creation. However, this cost is often negligible compared to the benefits of immutability, such as thread safety and data integrity. In performance-critical scenarios, it is recommended to use object pooling or caching techniques to reuse immutable objects and minimize the creation overhead.

Performance Consideration 2: Impact on Garbage Collection

Immutable objects can have a positive impact on garbage collection in Java. Since immutable objects cannot be modified, they do not generate garbage during their lifetime. This reduces the pressure on the garbage collector, resulting in improved overall performance and reduced memory footprint.

However, it is worth noting that if an immutable object contains mutable references, such as collections or arrays, the garbage collector will still need to collect any garbage generated by these mutable objects.

Performance Consideration 3: Use in Multithreaded Environments

Immutable objects are highly suitable for multi-threaded environments due to their thread safety. Since immutable objects cannot be modified after creation, they can be safely shared between multiple threads without the need for synchronization mechanisms like locks or mutexes. This eliminates the risk of data corruption or race conditions and simplifies the design and implementation of multi-threaded applications.

It is important to note that while immutable objects are thread-safe, concurrent modifications to mutable objects referred to by the immutable object may still require synchronization or other thread-safety mechanisms.

Related Article: How to Retrieve Current Date and Time in Java

Advanced Technique 1: Using Builder Pattern with Immutable Class

The Builder pattern is a useful technique to create complex immutable objects with a large number of fields. It provides a flexible and readable way to construct objects by using a fluent interface and separating the construction process from the object itself.

Here’s an example of an immutable class using the Builder pattern:

public final class Person {
    private final String firstName;
    private final String lastName;
    private final int age;
    
    private Person(String firstName, String lastName, int age) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }
    
    public String getFirstName() {
        return firstName;
    }
    
    public String getLastName() {
        return lastName;
    }
    
    public int getAge() {
        return age;
    }
    
    public static class Builder {
        private String firstName;
        private String lastName;
        private int age;
        
        public Builder setFirstName(String firstName) {
            this.firstName = firstName;
            return this;
        }
        
        public Builder setLastName(String lastName) {
            this.lastName = lastName;
            return this;
        }
        
        public Builder setAge(int age) {
            this.age = age;
            return this;
        }
        
        public Person build() {
            return new Person(firstName, lastName, age);
        }
    }
}

In this example, the Person class has three final fields representing the first name, last name, and age. The constructor is private to prevent direct instantiation. The static nested Builder class provides setter methods for each field, allowing the client code to set the desired values. The build() method constructs and returns an instance of the Person class.

To create a Person object using the Builder pattern, you can do the following:

Person person = new Person.Builder()
    .setFirstName("John")
    .setLastName("Doe")
    .setAge(30)
    .build();

By using the Builder pattern, you can construct immutable objects with a clear and concise syntax, especially when dealing with a large number of fields or optional parameters.

Advanced Technique 2: Immutable Class with List Objects

Immutable classes can handle collections like List by making defensive copies during object construction and getter methods. By doing so, the internal state of the immutable class remains unchanged, and the returned List is a separate instance.

Here’s an example of an immutable class with a List field:

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

public final class MyImmutableClass {
    private final List<String> values;
    
    public MyImmutableClass(List<String> values) {
        this.values = Collections.unmodifiableList(new ArrayList<>(values));
    }
    
    public List<String> getValues() {
        return Collections.unmodifiableList(new ArrayList<>(values));
    }
}

In this example, the MyImmutableClass has a List<String> field values. During object construction, a defensive copy of the values list is made using the ArrayList copy constructor, and it is wrapped with an unmodifiable list using Collections.unmodifiableList(). In the getter method, a new ArrayList is created from the values list, and it is wrapped with an unmodifiable list as well.

By making defensive copies and using unmodifiable lists, the MyImmutableClass ensures that the values list cannot be modified after object construction and that the returned list is read-only.

Advanced Technique 3: Immutable Class with Map Objects

Immutable classes can handle collections like Map by making defensive copies during object construction and getter methods. By doing so, the internal state of the immutable class remains unchanged, and the returned Map is a separate instance.

Here’s an example of an immutable class with a Map field:

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public final class MyImmutableClass {
    private final Map<String, Integer> values;
    
    public MyImmutableClass(Map<String, Integer> values) {
        this.values = Collections.unmodifiableMap(new HashMap<>(values));
    }
    
    public Map<String, Integer> getValues() {
        return Collections.unmodifiableMap(new HashMap<>(values));
    }
}

In this example, the MyImmutableClass has a Map<String, Integer> field values. During object construction, a defensive copy of the values map is made using the HashMap copy constructor, and it is wrapped with an unmodifiable map using Collections.unmodifiableMap(). In the getter method, a new HashMap is created from the values map, and it is wrapped with an unmodifiable map as well.

By making defensive copies and using unmodifiable maps, the MyImmutableClass ensures that the values map cannot be modified after object construction and that the returned map is read-only.

Related Article: How to Reverse a String in Java

Error Handling 1: Dealing with Incorrect Field Initialization

When creating immutable classes, it is important to handle incorrect field initialization gracefully. If a field is initialized with an invalid or unexpected value, it is recommended to throw an IllegalArgumentException or a custom exception to indicate the error.

Here’s an example of an immutable class with error handling for incorrect field initialization:

public final class Person {
    private final String firstName;
    private final String lastName;
    private final int age;
    
    public Person(String firstName, String lastName, int age) {
        if (firstName == null || lastName == null || age < 0) {
            throw new IllegalArgumentException("Invalid field value");
        }
        
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }
    
    public String getFirstName() {
        return firstName;
    }
    
    public String getLastName() {
        return lastName;
    }
    
    public int getAge() {
        return age;
    }
}

In this example, the Person class verifies the correctness of the firstName, lastName, and age fields during object construction. If any of these fields have invalid values, an IllegalArgumentException is thrown with an appropriate error message.

By handling incorrect field initialization, the immutable class ensures that objects are always created with valid and consistent state, preventing potential bugs or unexpected behavior.

Error Handling 2: Handling Exceptions during Object Creation

When creating immutable objects, it is important to handle exceptions that may occur during object construction. Any checked exceptions thrown by methods used for field initialization should be caught and properly handled. This ensures that the object creation process is robust and exceptions are not propagated to the client code.

Here’s an example of an immutable class handling exceptions during object creation:

public final class MyImmutableClass {
    private final int value;
    
    public MyImmutableClass(int value) {
        try {
            // Perform complex object initialization
        } catch (Exception e) {
            // Handle the exception appropriately
        }
        
        this.value = value;
    }
    
    public int getValue() {
        return value;
    }
}

In this example, the MyImmutableClass catches any exceptions that may occur during the complex object initialization process. The caught exception is then handled appropriately, ensuring that the value field is still initialized correctly.

By handling exceptions during object creation, the immutable class ensures that the object’s state remains consistent and that exceptions are properly dealt with.

How to Generate Random Integers in a Range in Java

Generating random integers within a specific range in Java is made easy with the Random class. This article explores the usage of java.util.Random and ThreadLocalRandom... read more

Java Equals Hashcode Tutorial

Learn how to implement equals and hashcode methods in Java. This tutorial covers the basics of hashcode, constructing the equals method, practical use cases, best... read more

How To Convert String To Int In Java

How to convert a string to an int in Java? This article provides clear instructions using two approaches: Integer.parseInt() and Integer.valueOf(). Learn the process and... read more

Java Composition Tutorial

This tutorial: Learn how to use composition in Java with an example. This tutorial covers the basics of composition, its advantages over inheritance, and best practices... read more

Java Hashmap Tutorial

This article provides a comprehensive guide on efficiently using Java Hashmap. It covers various use cases, best practices, real-world examples, performance... read more

Popular Data Structures Asked in Java Interviews

Tackle Java interview questions on popular data structures with this in-depth explanation. Learn about arrays, linked lists, stacks, queues, binary trees, hash tables,... read more