- Introduction to Immutable Class
- Definition of Immutable Class
- Characteristics of Immutable Class
- Use Case 1: Using Immutable Class for Thread Safety
- Use Case 2: Using Immutable Class for Cache Keys
- Use Case 3: Using Immutable Class for Map keys and Set elements
- Code Snippet 1: Creating an Immutable Class
- Code Snippet 2: Adding fields to an Immutable Class
- Code Snippet 3: Making Immutable Class with final keyword
- Code Snippet 4: Making Immutable Class with private constructor
- Code Snippet 5: Handling Date fields in Immutable Class
- Best Practice 1: Use final keyword for class
- Best Practice 2: Make constructor private
- Best Practice 3: Don’t provide setter methods
- Best Practice 4: Make all fields final and private
- Best Practice 5: Return a new object in getter methods
- Real World Example 1: Immutable Class in Financial Systems
- Real World Example 2: Immutable Class in Multi-threaded Applications
- Real World Example 3: Immutable Class in Cache Management Systems
- Performance Consideration 1: Creation Cost of Immutable Objects
- Performance Consideration 2: Impact on Garbage Collection
- Performance Consideration 3: Use in Multithreaded Environments
- Advanced Technique 1: Using Builder Pattern with Immutable Class
- Advanced Technique 2: Immutable Class with List Objects
- Advanced Technique 3: Immutable Class with Map Objects
- Error Handling 1: Dealing with Incorrect Field Initialization
- Error Handling 2: Handling Exceptions during Object Creation
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.