Immutable Class in Java
In this tutorial, we’ll learn about Immutable Class and its benefits in thread-safety, caching and collections. We will also look at rules to create immutable classes and eventually we’ll write an Immutable Class from scratch in Java.
What is Immutable Class?
Immutable Class means that once an object is initialized from this Class, we cannot change the state of that object.
In other words, An immutable object can’t be modified after it has been created. When a new value is needed, the accepted practice is to make a copy of the object that has the new value.
Examples
In Java, All primitive wrapper classes (Integer, Byte, Long, Float, Double, Character, Boolean and Short) and String are immutable in nature.
String is the most popular Immutable Class known among developers. String object cannot be modified once initialized. Operations like trim(), substring(), replace(), toUpperCase(), toLowerCase() always return a new instance and don’t affect the current instance.
In the below example, s1.toUpperCase() returns new instance which need to assign back to s1, if you want s1 to refer to new uppercase instance.
String s1 = new String("CodingNConcepts");
String s2 = s1.toUpperCase();
System.out.println(s1 == s2); //false
s1 = s2; //assign back to s1
System.out.println(s1 == s2); //true
Benefits of Immutable Class
Some of the key benefits of Immutable Class are:-
1. Mutable vs Immutable Object
A mutable object starts with one state (initial values of the instance variables). Each mutation of the instance variable takes the object to another state. This brings in the need to document all possible states a mutable object can be in. It is also difficult to avoid inconsistent or invalid states. Hence, all these makes a mutable object difficult to work with.
Whereas, immutable object have just one state, which is first and foremost benefit of Immutable Class.
2. Thread safety
Immutable objects are inherently thread-safe. They do not require synchronization. Since there is no way the state of an immutable object can change, there is no possibility of one thread observing the effect of another thread. We do not have to deal with the intricacies of sharing an object among threads (like locking, synchronization, making variables volatile etc.). Thus, we can freely share immutable objects. This is the easiest way to achieve thread safety.
3. Reusable/Cacheable
Immutable objects encourage to cache or store the frequently used instances rather than creating one each time. This is because two immutable instances with the same properties/values are equal.
Some examples of this being applied are as follows:-
Primitive wrapper classes
Creating primitive wrapper objects (Integer, Float, Double etc) using static factory method valueOf does not always return new wrapper instances. In case of Integer, they cache Integer values from -128 to 127 by default.
Integer oneV1 = new Integer(1);
Integer oneV2 = new Integer(1);
System.out.println(oneV1 == oneV2); //false
System.out.println(oneV1.equals(oneV2)); //true
oneV1 = Integer.valueOf(1); //returns cached instance
oneV2 = Integer.valueOf(1); //returns cached instance
System.out.println(oneV1 == oneV2); //true
System.out.println(oneV1.equals(oneV2)); //true
BigInteger
BigInteger stores some common BigInteger values as instance variables.
/**
* The BigInteger constant zero.
*/
public static final BigInteger ZERO = new BigInteger(new int[0], 0);
This reduces the memory footprint and the garbage collection costs.
4. Building blocks for Collections
The immutable objects make a great building block for Collections as compare to mutable objects. Let’s understand the problem we face with mutable objects in Collections.
First of all, we create a mutable Person class
class Person {
String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Person { name: " + name + " }";
}
}
Now let’s create some person objects to create Set of persons:-
Person person1 = new Person();
person1.setName("Adam");
Person person2 = new Person();
person2.setName("Ben");
Set<Person> setOfPerson = new HashSet<>(Arrays.asList(person1, person2));
System.out.println(setOfPerson);
person1.setName("Charlie");
System.out.println(setOfPerson);
Output
[Person { name: Adam }, Person { name: Ben }]
[Person { name: Charlie }, Person { name: Ben }]
We wanted to create a Set of persons Adam and Ben but next part of the code has mutated the Adam to Charlie which is not intended. We will solve this problem by making an immutable class ImmutablePerson in subsequent part of the article.
As we saw that mutable objects can be mutated even if not intended, immutable objects make a great fit to be used as Keys of a Map, in Set, List, and other collections.
How to create Immutable Class?
In order to create an Immutable Class, you should keep following points in mind:-
- Declare the class as final so that it cannot be extended and subclasses will not be able to override methods.
- Make all the fields as private so direct access in not allowed
- Make all the fields as final so that value cannot be modified once initialized
- Provide no setter methods — setter methods are those methods which modify fields or objects referred to by fields.
- Initialize all the final fields through a constructor and perform a deep copy for mutable objects.
- If the class holds a mutable object:
- Don’t provide any methods that modify the mutable objects.
- Always return a copy of mutable object from getter method and never return the actual object reference.
Let’s apply all the above points and create our immutable class ImmutablePerson
ImmutablePerson.java
/**
* Immutable class should mark as final so it can not be extended.
* Fields should mark as private so direct access is not allowed.
* Fields should mark as final so value can not be modified once initialized.
**/
public final class ImmutablePerson {
// String - immutable
private final String name;
// Integer - immutable
private final Integer weight;
// Date - mutable
private final Date dateOfBirth;
/**
* All the final fields are initialized through constructor
* Perform a deep copy of immutable objects
*/
public ImmutablePerson(String name, Integer weight, Date dateOfBirth){
this.name = name;
this.weight = weight;
this.dateOfBirth = new Date(dateOfBirth.getTime());
}
/**********************************************
***********PROVIDE NO SETTER METHODS *********
**********************************************/
/**
* String class is immutable so we can return the instance variable as it is
**/
public String getName() {
return name;
}
/**
* Integer class is immutable so we can return the instance variable as it is
**/
public Integer getWeight() {
return weight;
}
/**
* Date class is mutable so we need a little care here.
* We should not return the reference of original instance variable.
* Instead a new Date object, with content copied to it, should be returned.
**/
public Date getDateOfBirth() {
return new Date(dateOfBirth.getTime());
}
@Override
public String toString() {
return "Person { name: " + name + ", weight: " + weight + ", dateOfBirth: " + new SimpleDateFormat("dd-MM-yyyy").format(dateOfBirth) + "}";
}
}
Now, Let’s create immutable person objects to create Set of persons:-
ImmutablePerson person1 = new ImmutablePerson("Adam", 55, new SimpleDateFormat("dd-MM-yyyy").parse("01-01-2001"));
ImmutablePerson person2 = new ImmutablePerson("Ben", 50, new SimpleDateFormat("dd-MM-yyyy").parse("02-02-2002"));
Set<ImmutablePerson> setOfPerson = new HashSet<>(Arrays.asList(person1, person2));
System.out.println(setOfPerson);
/**
* ImmutablePerson do not provide setter methods,
* no way to mutate name, weight, or date property fields.
*/
//person1.setName("Charlie");
//person1.setWeight(90);
//person1.setDate(new SimpleDateFormat("dd-MM-yyyy").parse("03-03-2003"));
/**
* getDateOfBirth() method returns new instance of date,
* setYear() will not change value of person1's date field.
*/
Date person1Date = person1.getDateOfBirth();
person1Date.setYear(2020);
System.out.println(setOfPerson);
Output
[Person { name: Adam, weight: 55, dateOfBirth: 01-01-2001}, Person { name: Ben, weight: 50, dateOfBirth: 02-02-2002}]
[Person { name: Adam, weight: 55, dateOfBirth: 01-01-2001}, Person { name: Ben, weight: 50, dateOfBirth: 02-02-2002}]
We see that we can not mutate the collection of ImmutablePerson once created.
Summary
In this tutorial, we learned about Immutable Class in Java and its benefits. Moreover, Immutable Class is frequently asked interview question to check your understanding on design pattern, immutable & mutable objects and final keyword.
What do you think?
As per Oracle Docs for Immutable Classes, Don’t allow subclasses to override methods. The simplest way to do this is to declare the class as final. A more sophisticated approach is to make the constructor private and construct instances in factory methods.
Please note that we have marked the class as final and constructor as public in our ImmutablePerson class.
It is debatable. Should we use private constructor with static factory method to create instances? My view is we are restricting the instance creation by using private constructor, which is not a desired scenario for immutability.
What’s your thoughts on this? Please comment.