In Java, immutable objects are objects whose state cannot be changed once they are created; they will remain constant throughout the lifecycle of the object. It helps in terms of simplicity, thread-safety and security. For example, string objects, In Java, string objects are immutable. i.e., once they are initialized, they cannot be changed. To learn more about the immutability of a string class, please refer Why use the char[] array for storing passwords over strings in Java?. In this tutorial, we will learn how to create immutable object in Java.
Rules to create immutable object in Java
In Java, we can create an immutable object with the help of following rules:
- Make the class “final”: This will prevent the class from being extended and modified from the other classes.
- Make all the fields “private”: Making fields private will not allow the direct access to the fields.
- Do not provide setter methods: Avoiding setters will ensure not to modify the state of object.
- Make fields “final” that are mutable: It ensures that once the value is assigned to the fields, it can not be reassigned.
- Deep copy in Constructor: This ensures that the internal state of the object remains truly immutable by preventing external modification through references to mutable objects.
- Deep copy in getter: If we have mutable objects as fields in a class and we want to ensure that clients of our class cannot modify the internal state, we might consider returning a copy of the field rather than a direct reference.
1. Mutable object in Java
First, we will understand what a mutable object is. For this, we will be using an employee example. We first create its object, and then we change its attributes to understand its mutable behaviour.
1.1- Create Employee class
First of all, we need a class for the object. For this, we are going to create an Employee class.
import java.util.List;
import java.util.Objects;
/**
* 1. Employee<id, name, role, technologies>
* 2. toString()- used to print the Object's
* 3. value hashCode()- JVM will assign a unique value to every object, this method will return that unique value
*
* @author paulsofts
*/
public class Employee {
int id;
String name;
String role;
List<String> technologies;
public Employee(int id, String name, String role, List<String> technologies) {
super();
this.id = id;
this.name = name;
this.role = role;
this.technologies = technologies;
}
@Override
public String toString() {
return "Employee [id=" + id + ", name=" + name + ", role=" + role + ", technologies=" + technologies + "]";
}
@Override
public int hashCode() {
return Objects.hash(id, name, role, technologies);
}
}
1.2- Main class
Now, we will need a Main class that contains the main (String[] args) method to start the program execution.
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
/**
* 1. List<String> list: contains the technologies employee knows
* 2. We are using all args constructor to create employee object
* 3. init(): used to print the hashcode and the employee details
*
* @author paulsofts
*/
public class Main {
Logger logger = Logger.getLogger(getClass().getName());
public static void main(String[] args) {
Main main = new Main();
List<String> list = new ArrayList<>();
list.add("Java");
list.add("Spring Boot");
list.add("Jenkins");
list.add("Kafka");
list.add("MySQL");
list.add("Mongo");
Employee employee = new Employee(101, "Ankur", "Developer", list);
main.init(employee);
}
public void init(Employee employee) {
logger.info("Object hashcode: " + employee.hashCode());
logger.info("Object details: " + employee);
}
}
Below is the output for the following: As we can see, we are getting the hashcode and the object details in the logs.
1.3- Update the employee name
In the Main class, we will update the name of the employee using employee.name and change it to some other name.
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
/**
* 1. List<String> list: contains the technologies employee knows
* 2. We are using all args constructor to create employee object
* 3. Update the name of employee
* 4. init(): used to print the hashcode and the employee details
*
* @author paulsofts
*/
public class Main {
Logger logger = Logger.getLogger(getClass().getName());
public static void main(String[] args) {
Main main = new Main();
List<String> list = new ArrayList<>();
list.add("Java");
list.add("Spring Boot");
list.add("Jenkins");
list.add("Kafka");
list.add("MySQL");
list.add("Mongo");
Employee employee = new Employee(101, "Ankur", "Developer", list);
//Update the name of the employee
employee.name = "Sahil";
main.init(employee);
}
public void init(Employee employee) {
logger.info("Object hashcode: " + employee.hashCode());
logger.info("Object details: " + employee);
}
}
After we have updated the name of employee, we are getting following output:
As we see, the hashcode value gets changed, which means the employee object gets changed, and so the employee object is not immutable.
2. Create immutable object in Java
We use the above-mentioned rules and update our employee class so that its objects become immutable in nature.
2.1 Make Employee class final
We make our Employee class final; the final keyword will ensure that our Employee class cannot be extended or modified by some other classes.
2.2 Make all fields as private
We make all the fields (member variables) of our Employee class as private. It prevent the direct access to the member variables of the class.
2.3 Do not provide setter methods
After we have made all the member variables of the Employee class private, to access them, we need getter methods. But, make sure not to provide the setter methods, as they can be used to modify the state of the employee object.
Until now, our employee class will have the following structure:
import java.util.List;
import java.util.Objects;
/**
* 1. Employee<id, name, role, technologies>
* 2. toString()- used to print the Object's
* 3. value hashCode()- JVM will assign a unique value to every object, this method will return that unique value
*
* How to create immutable object?
* a) First, we are making Employee class as final
* b) Making all fields as private
* c) Add getter and remove setter methods
*
* @author paulsofts
*/
public final class Employee {
private int id;
private String name;
private String role;
private List<String> technologies;
public Employee(int id, String name, String role, List<String> technologies) {
super();
this.id = id;
this.name = name;
this.role = role;
this.technologies = technologies;
}
public int getId() {
return id;
}
public String getName() {
return name;
}
public String getRole() {
return role;
}
public List<String> getTechnologies() {
return technologies;
}
@Override
public String toString() {
return "Employee [id=" + id + ", name=" + name + ", role=" + role + ", technologies=" + technologies + "]";
}
@Override
public int hashCode() {
return Objects.hash(id, name, role, technologies);
}
}
2.4 Make fields final
In this step, we need to make the fields final, which can be mutable. For example, in our Employee class, we have a field (id, which is of int type) that can be changed. So, we need to make this field final. But, on the safe side, we are making every field as final.
2.5 Deep copy constructor
This is important, we have to make sure we use deep copy constructor in our class. Let’s understand this in detail, we have a field (List<String> technologies) in our Employee class constructor.
public Employee(int id, String name, String role, List<String> technologies) {
super();
this.id = id;
this.name = name;
this.role = role;
this.technologies = technologies;
}
It is a reference variable that contains references to other string values, which means if they change, the reference of the list will also change, making it mutable. In other words, if the list values get changed, it will change the address of the complete list variable, and the changed address will be assigned to the employee class list variable. In order to overcome this issue, we will use deep copy constructor.
public Employee(int id, String name, String role, List<String> technologies) {
super();
this.id = id;
this.name = name;
this.role = role;
List<String> temp = new ArrayList<>();
for(String str : technologies) {
temp.add(str);
}
this.technologies = temp;
}
2.6 Deep copy getter
Similar to the deep copy inside the constructor, we also need the deep copy inside the getter method for our list variable.
public List<String> getTechnologies() {
List<String> temp = new ArrayList<>();
for(String str : technologies) {
temp.add(str);
}
return temp;
}
Note: Make sure to take care of all the reference variables inside a class while making it mutable.
Now, we have successfully made our employee class immutable. As soon as we create an object of this class, we have an immutable object.
Employee.class
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* 1. Employee<id, name, role, technologies>
* 2. toString()- used to print the Object's
* 3. value hashCode()- JVM will assign a unique value to every object, this method will return that unique value
*
* How to create immutable object?
* a) First, we are making Employee class as final
* b) Making all fields as private
* c) Add getter and remove setter methods
* d) Making all the field final
* e) Deep copy inside constructor
* f) Deep copy inside getter
*
* @author paulsofts
*/
public final class Employee {
private final int id;
private final String name;
private final String role;
private final List<String> technologies;
public Employee(int id, String name, String role, List<String> technologies) {
super();
this.id = id;
this.name = name;
this.role = role;
List<String> temp = new ArrayList<>();
for(String str : technologies) {
temp.add(str);
}
this.technologies = temp;
}
public int getId() {
return id;
}
public String getName() {
return name;
}
public String getRole() {
return role;
}
public List<String> getTechnologies() {
List<String> temp = new ArrayList<>();
for(String str : technologies) {
temp.add(str);
}
return temp;
}
@Override
public String toString() {
return "Employee [id=" + id + ", name=" + name + ", role=" + role + ", technologies=" + technologies + "]";
}
@Override
public int hashCode() {
return Objects.hash(id, name, role, technologies);
}
}
Main.class
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
/**
* 1. List<String> list: contains the technologies employee knows
* 2. We are using all args constructor to create employee object
* 3. Update the name of employee
* 4. init(): used to print the hashcode and the employee details
*
* @author paulsofts
*/
public class Main {
Logger logger = Logger.getLogger(getClass().getName());
public static void main(String[] args) {
Main main = new Main();
List<String> list = new ArrayList<>();
list.add("Java");
list.add("Spring Boot");
list.add("Jenkins");
list.add("Kafka");
list.add("MySQL");
list.add("Mongo");
Employee employee = new Employee(101, "Ankur", "Developer", list);
main.init(employee);
}
public void init(Employee employee) {
logger.info("Object hashcode: " + employee.hashCode());
logger.info("Object details: " + employee);
logger.info("Technologies list: " + employee.getTechnologies());
}
}
As we run our Main.class we will get the following output:
2.7 Testing
In this step, we will test our immutable Employee class object. We do not have a setter method in our employee class. So, we cannot directly set data for it. Instead, we will get data, change it, and then test for the immutability of employee objects.
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
/**
* 1. List<String> list: contains the technologies employee knows
* 2. We are using all args constructor to create employee object
* 3. Update the name of employee
* 4. init(): used to print the hashcode and the employee details
*
* @author paulsofts
*/
public class Main {
Logger logger = Logger.getLogger(getClass().getName());
public static void main(String[] args) {
Main main = new Main();
List<String> list = new ArrayList<>();
list.add("Java");
list.add("Spring Boot");
list.add("Jenkins");
list.add("Kafka");
list.add("MySQL");
list.add("Mongo");
Employee employee = new Employee(101, "Ankur", "Developer", list);
//fetching the list of technologies
List<String> tech_list = employee.getTechnologies();
//clear the list- it will empty the list
tech_list.clear();
System.out.println("tech_list contains: " + tech_list);
main.init(employee);
}
public void init(Employee employee) {
logger.info("Object hashcode: " + employee.hashCode());
logger.info("Object details: " + employee);
logger.info("Technologies list: " + employee.getTechnologies());
}
}
In the above code snippet, we have created tech_list using getter method of employee class and after ward we have cleared the list. We will get the following output for above program:
If we do not do the deep copy inside the constructor, the employee object will get changed. This is because, as we get the list of technologies using getter, it will bring the reference to that list and assign it to the tech_list, and as we clear the tech_list, it will clear values present at that reference and change the object. To understand this, below we have updated the employee class getter for the getTechnologies() method.
package com.paulsofts.Collections.ImmutableObject;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* 1. Employee<id, name, role, technologies>
* 2. toString()- used to print the Object's
* 3. value hashCode()- JVM will assign a unique value to every object, this method will return that unique value
*
* How to create immutable object?
* a) First, we are making Employee class as final
* b) Making all fields as private
* c) Add getter and remove setter methods
* d) Making all the field final
* e) Deep copy inside constructor
* f) Deep copy inside getter
*
* @author paulsofts
*/
public final class Employee {
private final int id;
private final String name;
private final String role;
private final List<String> technologies;
public Employee(int id, String name, String role, List<String> technologies) {
super();
this.id = id;
this.name = name;
this.role = role;
List<String> temp = new ArrayList<>();
for(String str : technologies) {
temp.add(str);
}
this.technologies = temp;
}
public int getId() {
return id;
}
public String getName() {
return name;
}
public String getRole() {
return role;
}
public List<String> getTechnologies() {
// List<String> temp = new ArrayList<>();
// for(String str : technologies) {
// temp.add(str);
// }
// return temp;
return technologies;
}
@Override
public String toString() {
return "Employee [id=" + id + ", name=" + name + ", role=" + role + ", technologies=" + technologies + "]";
}
@Override
public int hashCode() {
return Objects.hash(id, name, role, technologies);
}
}
Now, if we run our main class, the object will get changed.
Above, we can see the hascode for the object has changed along with the list of technologies becoming empty.