Understanding Immutable Objects in Java
Explore advanced Java concepts with this in-depth guide to understanding immutability. Learn how immutability enhances thread safety, performance, and design in Java applications.
What is Immutability?
Before I discuss records and why they are needed, I need to articulate the concept of immutability. Immutability is a key aspect of clean and safe programming.
Let us define immutability - an immutable object is one whose state cannot be changed once instantiated, where the state is the data contained in the object instance. When an object’s state is set, it stays the same throughout its lifetime. In Java, for example, immutable objects do not have any setter methods to guarantee their state never changes.
Examples of Immutable Objects
Java’s standard library is rich with immutable classes, including:
String
- Wrapper classes for primitives (e.g.
Integer
,Double
) BigInteger
andBigDecimal
- Date and time classes from the
java.time
package
These classes demonstrate the effectiveness of immutability in various contexts, from text processing to complex arithmetic and date-time manipulation.
Why Choose Immutability?
Immutability is considered as best practice in many scenarios:
Simplified Reasoning: Immutable objects make programs easier to understand and debug. When an object’s state cannot change unexpectedly, you can reason about its behaviour with confidence.
Thread Safety: Immutable objects shine in multi-threaded environments. They are inherently thread-safe, eliminating the need for complicated synchronization mechanisms.
Reliability in Collections: Immutable objects are perfect for use as keys in
HashMap
or elements inHashSet
because their hash codes never change, ensuring data integrity.Performance and Memory Efficiency: Immutable objects can often be shared and reused, reducing memory overhead. For example, the JVM’s string pool allows reusing
String
objects to save memory and enhance performance. Java also reuse any existing objects while autoboxing and wrapping primitive values.
Challenges of Immutability
While immutability has many advantages, it’s not without trade-offs:
Efficiency Concerns: Modifying immutable objects is done by creating new instances. This, under certain conditions, leads to serious performance issues; for example, the concatenation of strings within a loop does not work well since every time a new
String
instance will be created. A better approach is to useStringBuilder
, which is designed to mutate for such scenarios.Circular References: Creating circular references among immutable objects can present challenges. For example, when objects X and Y are required to reference one another. If objects are mutable, that is easy, initialise the fields at the time of creation. But, accomplishing this while preserving immutability is a challenge. It becomes a chicken-and-egg problem, where Object A must initialise with reference to B which does not exist yet and vice versa. I can argue that it is not really a disadvantage as circular references are a code smell and show coupling.
These challenges notwithstanding, the benefits of immutability often outweigh its costs. Immutability promotes better design by avoiding problems such as tightly coupled classes, and it encourages separation of concerns.
Creating an Immutable Class
Sample Product POJO
Let me first create a usual POJO Product
class that contains information about the product. I will add below three attributes to the class
id
of typelong
to uniquely identify the productname
of typestring
to hold the name of the productdescription
of typestring
to describe the product
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package demo;
public class Product {
private long id;
private String name;
private String description;
public Product(long id, String name, String description) {
this.id = id;
this.name = name;
this.description = description;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}
Immutability Strategies
There is a list of rules that we need to apply to make the above class immutable. Let me first list them down:
- I first remove all the
setter
methods to restrict the modification of fields. - I will make all fields
final
andprivate
. - I will make the class
final
to restrict mutable subclasses and use override methods. - I will make sure that instance fields do not have any reference to mutable objects (I will cover this in detail in later articles).
- Finally, I will also provide
equals()
,hashCode()
andtoString()
methods for this class.
Now that I know, what needs to be done, I will move on to it next.
Immutable Product
Here is the immutable Product
.
As you can observe, there are quite a few considerations and the code is verbose. Although a lot of code I have generated is through my IDE, it is still verbose.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public final class Product {
private final long id;
private final String name;
private final String description;
public Product(long id, String name, String description) {
this.id = id;
this.name = name;
this.description = description;
}
public long getId() {
return id;
}
public String getName() {
return name;
}
public String getDescription() {
return description;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Product product = (Product) o;
return id == product.id && Objects.equals(name, product.name) && Objects.equals(description, product.description);
}
@Override
public int hashCode() {
return Objects.hash(id, name, description);
}
@Override
public String toString() {
return "Product{" +
"id=" + id +
", name='" + name + '\'' +
", description='" + description + '\'' +
'}';
}
}
Wrap Up
By leveraging immutable objects, you write more predictable, maintainable, and robust code. But there are easier ways to achieve all of the above. This is where Record
classes come in handy and allow the creation of immutable objects much easier. In my next post, I will introduce and discuss Record
classes.
Photo Credits
Header page image by Markus Spiske on Unsplash