Introduction
This article provides an overview of how to create an immutable class in Java programming.
An object is immutable when its state doesn’t change after it has been initialized. For example, String
is an immutable class and, once instantiated, the value of a String
object never changes. Learn more about why the String
class is immutable in Java.
Because an immutable object can’t be updated, programs need to create a new object for every change of state. However, immutable objects also have the following benefits:
- An immutable class is good for caching purposes because you don’t have to worry about the value changes.
- An immutable class is inherently thread-safe, so you don’t have to worry about thread safety in multi-threaded environments.
Learn more about multi-threading in Java and browse the Java Multi-Threading Interview Questions.
Creating an Immutable Class in Java
To create an immutable class in Java, you need to follow these general principles:
- Declare the class as
final
so it can’t be extended. - Make all of the fields
private
so that direct access is not allowed. - Don’t provide setter methods for variables.
- Make all mutable fields
final
so that a field’s value can be assigned only once. - Initialize all fields using a constructor method performing deep copy.
- Perform cloning of objects in the getter methods to return a copy rather than returning the actual object reference.
The following class is an example that illustrates the basics of immutability. The FinalClassExample
class defines the fields and provides the constructor method that uses deep copy to initialize the object. The code in the main
method of the FinalClassExample.java
file tests the immutability of the object.
Create a new file called FinalClassExample.java
and copy in the following code:
import java.util.HashMap;
import java.util.Iterator;
public final class FinalClassExample {
// fields of the FinalClassExample class
private final int id;
private final String name;
private final HashMap<String,String> testMap;
public int getId() {
return id;
}
public String getName() {
return name;
}
// Getter function for mutable objects
public HashMap<String, String> getTestMap() {
return (HashMap<String, String>) testMap.clone();
}
// Constructor method performing deep copy
public FinalClassExample(int i, String n, HashMap<String,String> hm){
System.out.println("Performing Deep Copy for Object initialization");
// "this" keyword refers to the current object
this.id=i;
this.name=n;
HashMap<String,String> tempMap=new HashMap<String,String>();
String key;
Iterator<String> it = hm.keySet().iterator();
while(it.hasNext()){
key=it.next();
tempMap.put(key, hm.get(key));
}
this.testMap=tempMap;
}
// Test the immutable class
public static void main(String[] args) {
HashMap<String, String> h1 = new HashMap<String,String>();
h1.put("1", "first");
h1.put("2", "second");
String s = "original";
int i=10;
FinalClassExample ce = new FinalClassExample(i,s,h1);
// print the ce values
System.out.println("ce id: "+ce.getId());
System.out.println("ce name: "+ce.getName());
System.out.println("ce testMap: "+ce.getTestMap());
// change the local variable values
i=20;
s="modified";
h1.put("3", "third");
// print the values again
System.out.println("ce id after local variable change: "+ce.getId());
System.out.println("ce name after local variable change: "+ce.getName());
System.out.println("ce testMap after local variable change: "+ce.getTestMap());
HashMap<String, String> hmTest = ce.getTestMap();
hmTest.put("4", "new");
System.out.println("ce testMap after changing variable from getter methods: "+ce.getTestMap());
}
}
Compile and run the program:
- javac FinalClassExample.java
- java FinalClassExample
Note: You might get the following message when you compile the file: Note: FinalClassExample.java uses unchecked or unsafe operations
because the getter method is using an unchecked cast from HashMap<String,String>
to Object
. You can ignore the compiler warning for the purposes of this example.
You get the following output:
OutputPerforming Deep Copy for Object initialization
ce id: 10
ce name: original
ce testMap: {1=first, 2=second}
ce id after local variable change: 10
ce name after local variable change: original
ce testMap after local variable change: {1=first, 2=second}
ce testMap after changing variable from getter methods: {1=first, 2=second}
The output shows that the HashMap values didn’t change because the constructor uses deep copy and the getter function returns a clone of the original object.
What happens when you don’t use deep copy and cloning
You can make changes to the FinalClassExample.java
file to show what happens when you use shallow copy instead of deep copy and return the object insetad of a copy. The object is no longer immutable and can be changed. Make the following changes to the example file (or copy and paste from the code example):
- Delete the constructor method providing deep copy and add the constructor method providing shallow copy that is highlighted in the following example.
- In the getter function, delete
return (HashMap<String, String>) testMap.clone();
and addreturn testMap;
.
The example file should now look like this:
import java.util.HashMap;
import java.util.Iterator;
public final class FinalClassExample {
// fields of the FinalClassExample class
private final int id;
private final String name;
private final HashMap<String,String> testMap;
public int getId() {
return id;
}
public String getName() {
return name;
}
// Getter function for mutable objects
public HashMap<String, String> getTestMap() {
return testMap;
}
//Constructor method performing shallow copy
public FinalClassExample(int i, String n, HashMap<String,String> hm){
System.out.println("Performing Shallow Copy for Object initialization");
this.id=i;
this.name=n;
this.testMap=hm;
}
// Test the immutable class
public static void main(String[] args) {
HashMap<String, String> h1 = new HashMap<String,String>();
h1.put("1", "first");
h1.put("2", "second");
String s = "original";
int i=10;
FinalClassExample ce = new FinalClassExample(i,s,h1);
// print the ce values
System.out.println("ce id: "+ce.getId());
System.out.println("ce name: "+ce.getName());
System.out.println("ce testMap: "+ce.getTestMap());
// change the local variable values
i=20;
s="modified";
h1.put("3", "third");
// print the values again
System.out.println("ce id after local variable change: "+ce.getId());
System.out.println("ce name after local variable change: "+ce.getName());
System.out.println("ce testMap after local variable change: "+ce.getTestMap());
HashMap<String, String> hmTest = ce.getTestMap();
hmTest.put("4", "new");
System.out.println("ce testMap after changing variable from getter methods: "+ce.getTestMap());
}
}
Compile and run the program:
- javac FinalClassExample.java
- java FinalClassExample
You get the following output:
OutputPerforming Shallow Copy for Object initialization
ce id: 10
ce name: original
ce testMap: {1=first, 2=second}
ce id after local variable change: 10
ce name after local variable change: original
ce testMap after local variable change: {1=first, 2=second, 3=third}
ce testMap after changing variable from getter methods: {1=first, 2=second, 3=third, 4=new}
The output shows that the HashMap values got changed because the constructor method uses shallow copy there is a direct reference to the original object in the getter function.
Conclusion
You’ve learned some of the general principles to follow when you create immutable classes in Java, including the importance of deep copy. Continue your learning with more Java tutorials.
Source:
https://www.digitalocean.com/community/tutorials/how-to-create-immutable-class-in-java