How to Create Immutable Classes in Java

Avatar

By squashlabs, Last Updated: Aug. 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 Work with Strings 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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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;
}
}
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; } }
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
ImmutablePoint class has two final fields
x
x and
y
y, which are set during object construction and cannot be modified thereafter. This ensures that the state of the
ImmutablePoint
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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
public final class CacheKey {
private final String key;
public CacheKey(String key) {
this.key = key;
}
public String getKey() {
return key;
}
}
public final class CacheKey { private final String key; public CacheKey(String key) { this.key = key; } public String getKey() { return 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
CacheKey class has a single final field
key
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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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);
}
}
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); } }
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
Person class has two final fields
name
name and
age
age, which are set during object construction and cannot be modified thereafter. The class also overrides the
hashCode()
hashCode() and
equals()
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
final to prevent inheritance.

2. Declare all fields

private
private and
final
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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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;
}
}
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; } }
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
Book class is declared
final
final to prevent inheritance. The fields
title
title,
author
author, and
publicationYear
publicationYear are declared
private
private and
final
final to ensure immutability. The constructor initializes these fields, and getter methods are provided to access their values.

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
genre field to the
Book
Book class:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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;
}
}
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; } }
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
genre field is added to the
Book
Book class along with its getter method. The constructor is updated to accept the
genre
genre as a parameter and initialize the field accordingly.

Related Article: How to Use the Xmx Option in Java

Code Snippet 3: Making Immutable Class with final keyword

One way to create an immutable class in Java is by using the

final
final keyword. By making the class
final
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
final keyword:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
public final class MyImmutableClass {
private final int value;
public MyImmutableClass(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
public final class MyImmutableClass { private final int value; public MyImmutableClass(int value) { this.value = value; } public int getValue() { return value; } }
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
MyImmutableClass is declared
final
final, and the
value
value field is declared
final
final as well. The constructor initializes the
value
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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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);
}
}
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); } }
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
MyImmutableClass has a private constructor, preventing direct instantiation. Instead, a static factory method
create()
create() is provided to create instances of the class. The
value
value field is declared
final
final, ensuring immutability.

Code Snippet 5: Handling Date fields in Immutable Class

Handling

Date
Date fields in an immutable class requires special attention because
Date
Date is mutable. To ensure immutability, you should make a defensive copy of the
Date
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
Date field:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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());
}
}
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()); } }
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
MyImmutableClass has a
Date
Date field
date
date. During object construction, a defensive copy of the
date
date object is made using the
getTime()
getTime() method to obtain the underlying
long
long value. In the getter method, a new
Date
Date object is created using the
long
long value of the
date
date field, ensuring that the returned
Date
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
final keyword for the class declaration. By making the class
final
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
final keyword:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
public final class MyImmutableClass {
// Fields and methods
}
public final class MyImmutableClass { // Fields and methods }
public final class MyImmutableClass {
    // Fields and methods
}

In this example, the

MyImmutableClass
MyImmutableClass is declared
final
final, preventing any subclassing.

Related Article: How to Implement a Delay in Java Using java wait seconds

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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
public final class MyImmutableClass {
private MyImmutableClass() {
// Private constructor
}
// Fields and methods
}
public final class MyImmutableClass { private MyImmutableClass() { // Private constructor } // Fields and methods }
public final class MyImmutableClass {
    private MyImmutableClass() {
        // Private constructor
    }
    
    // Fields and methods
}

In this example, the

MyImmutableClass
MyImmutableClass has a private constructor, preventing direct instantiation.

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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
public final class MyImmutableClass {
private final int value;
public MyImmutableClass(int value) {
this.value = value;
}
public int getValue() {
return value;
}
// No 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 }
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
MyImmutableClass has a single final field
value
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
final and
private
private. By making the fields
final
final, their values cannot be changed once they are set. By making the fields
private
private, their values can only be accessed through getter methods.

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
public final class MyImmutableClass {
private final int value;
public MyImmutableClass(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
public final class MyImmutableClass { private final int value; public MyImmutableClass(int value) { this.value = value; } public int getValue() { return value; } }
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
MyImmutableClass has a single final field
value
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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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);
}
}
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); } }
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
MyImmutableClass has a
List<String>
List<String> field
values
values. The getter method
getValues()
getValues() returns a new
ArrayList
ArrayList containing the elements of the
values
values field. This ensures that the returned list is a separate instance and cannot be modified externally.

Related Article: How to Generate Random Numbers in Java

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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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;
}
}
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; } }
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
FinancialTransaction class is declared
final
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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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);
}
}
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); } }
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
Configuration class has a
Map<String, String>
Map<String, String> field
properties
properties. The constructor makes a defensive copy of the
properties
properties map using the
HashMap
HashMap copy constructor and wraps it with an unmodifiable map using
Collections.unmodifiableMap()
Collections.unmodifiableMap(). This ensures that the
properties
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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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);
}
}
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); } }
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
CacheKey class has a single final field
key
key, which is set during object construction and cannot be modified thereafter. The class overrides the
hashCode()
hashCode() and
equals()
equals() methods to ensure proper behavior when used as a key in a cache.

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.

Related Article: How to Initialize an ArrayList in One Line in Java

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.

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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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);
}
}
}
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); } } }
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
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
Builder class provides setter methods for each field, allowing the client code to set the desired values. The
build()
build() method constructs and returns an instance of the
Person
Person class.

To create a

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
Person person = new Person.Builder()
.setFirstName("John")
.setLastName("Doe")
.setAge(30)
.build();
Person person = new Person.Builder() .setFirstName("John") .setLastName("Doe") .setAge(30) .build();
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
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
List is a separate instance.

Here's an example of an immutable class with a

List
List field:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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));
}
}
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)); } }
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
MyImmutableClass has a
List<String>
List<String> field
values
values. During object construction, a defensive copy of the
values
values list is made using the
ArrayList
ArrayList copy constructor, and it is wrapped with an unmodifiable list using
Collections.unmodifiableList()
Collections.unmodifiableList(). In the getter method, a new
ArrayList
ArrayList is created from the
values
values list, and it is wrapped with an unmodifiable list as well.

By making defensive copies and using unmodifiable lists, the

MyImmutableClass
MyImmutableClass ensures that the
values
values list cannot be modified after object construction and that the returned list is read-only.

Related Article: How to Easily Print a Java Array

Advanced Technique 3: Immutable Class with Map Objects

Immutable classes can handle collections like

Map
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
Map is a separate instance.

Here's an example of an immutable class with a

Map
Map field:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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));
}
}
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)); } }
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
MyImmutableClass has a
Map<String, Integer>
Map<String, Integer> field
values
values. During object construction, a defensive copy of the
values
values map is made using the
HashMap
HashMap copy constructor, and it is wrapped with an unmodifiable map using
Collections.unmodifiableMap()
Collections.unmodifiableMap(). In the getter method, a new
HashMap
HashMap is created from the
values
values map, and it is wrapped with an unmodifiable map as well.

By making defensive copies and using unmodifiable maps, the

MyImmutableClass
MyImmutableClass ensures that the
values
values map cannot be modified after object construction and that the returned map is read-only.

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
IllegalArgumentException or a custom exception to indicate the error.

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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;
}
}
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; } }
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
Person class verifies the correctness of the
firstName
firstName,
lastName
lastName, and
age
age fields during object construction. If any of these fields have invalid values, an
IllegalArgumentException
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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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;
}
}
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; } }
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
MyImmutableClass catches any exceptions that may occur during the complex object initialization process. The caught exception is then handled appropriately, ensuring that the
value
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.

Java Code Snippets for Everyday Problems

This article is a detailed guide that provides 22 Java code snippets to solve common issues. From calculating factorial to checking if two strings ar… read more

Identifying the Version of Your MySQL-Connector-Java

Determining the version of your MySQL-Connector-Java is essential for Java developers working with MySQL databases. In this article, we will guide yo… 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,… read more

How to Implement Recursion in Java

Recursion is a fundamental concept in Java programming, and understanding the data structure that controls recursion is essential for every software … read more

Tutorial: Enumeration Types and Data Structures in Java

Enumeration types in Java provide a powerful way to define a set of named values. In this article, we will explore how enumeration types can be used … read more

How to Use Multithreading in Java

Learn how to implement multithreading in Java for concurrent programming. This article covers the introduction and concepts of multithreading, use ca… 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(). L… read more

Top Java Interview Questions and Answers

Java programming is a fundamental skill for any aspiring software engineer. This article provides essential interview questions and answers to help y… read more

Tutorial on Integrating Redis with Spring Boot

This guide explains how to integrate Redis into a Spring Boot application. It covers topics such as setting up Redis, basic and advanced usage, and u… read more

Java Hibernate Interview Questions and Answers

Hibernate is a popular object-relational mapping (ORM) tool in Java development. In this article, we will explore Hibernate interview questions and a… read more