In this article, we will look how to create custom immutable class in Java. Immutable objects are those objects whose states cannot be changed. For example, you can change the state of an object by changing its attribute’s or field’s value. String is an example of an immutable class in Java.
Other examples of immutable classes in Java API, all primitive wrapper classes are immutable classes – Integer, Byte, Long, Float, Double, Character, Boolean, Short. In Java 8, the Date Time API made all classes to be immutable, for example, LocalDate, LocalTime, LocalDateTime, etc.
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, we don’t have to worry about the value changes.
- An immutable class is inherently thread-safe, we don’t have to worry about thread safety in multi-threaded environments.
Steps to create Immutable Objects
The following rules define a simple strategy for creating immutable objects in Java:
- Declare the class as final, this will prevent your class to be sub-classed for overriding methods.
- Make all fields or attributes in a class as private. so that direct access is not allowed.
- Make all mutable fields
final
so that a field’s value can be assigned only once. - Do not provide any setter methods for fields in the class.
- Initialize all fields through constructor or in the factory method
- If the instance fields include references to the mutable objects, don’t allow those objects to be changed:
- Don’t provide methods that modify mutable objects
- Don’t share references to the mutable objects. Never store references to external, mutable objects passed to the constructor; if necessary, create copies, and store references to the copies. Similarly, create copies of your internal mutable objects when necessary to avoid returning the originals in your methods
Advantages of Immutable Objects
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:
- Immutable objects are inherently thread-safe. They can be easily used in a multi-threaded environment
- Immutable objects do not change their value, we can used it for caching purpose.
- Immutable objects are good candidate for hash keys because their hash code can be cached and reused for better performance. That’s why it is recommended to use immutable object as a key in HashMap Collection classes.
Creating Immutable class
Let’s learn how to create a Example immutable class with the following fields: id, name, and subjects:
public class ClassStudent{ String studentName; Integer studentId; List <String> subject; }
1. Make the class Final so that it cannot be extended.
public class final ClassStudent{ String studentName; Integer studentId; List < String > subject; }
2. Make all fields private so that direct access is prohibited.
public class final ClassStudent{ private String studentName; private Integer studentId; private List < String > subject; }
3. Don’t provide variable setter methods.
public class final ClassStudent{ private String studentName; private Integer studentId; private List < String > subject; public String getstudentName() { return this.studentName; } public Integer getstudentId() { return this.studentId; } public List < String > getsubject() { return this.subject; } }
4. Make all mutual fields final, – this means their values cannot be changed.
public class final ClassStudent{ private String studentName; private final Integer studentId; private final List < String > subject; public String getstudentName() { return this.studentName; } public String getstudentId() { return this.studentId; } public List < String > getsubject() { return this.subject; } }
5. Using deep copy, invoke all fields by constructors.
public class final ClassStudent{ private String studentName; private final Integer studentId; private final List < String > subject; public ClassStudent(String name, Integr id, List < String > subject) { this.studentName = name; this.studentId = id; this.subject = subject; } public String getstudentName() { return this.studentName; } public String getstudentId() { return this.studentId; } public List < String > getsubject() { return this.subject; } }
6. Clone objects in getter methods to return a duplicate rather than the original object reference.
public class final ClassStudent{ private String studentName; private final Integer studentId; private final List < String > subject; public ClassStudent(String name, Integr id, List < String > subject) { this.studentName = name; this.studentId = id; this.subject = subject; } public String getstudentName() { return this.studentName; } public String getstudentId() { return this.studentId; } public List < String > getsubject() { return this.subject.clone(); } }
Example of Immutable class in Java
The following class is an example that illustrates the basics of immutability
import java.util.LinkedList;
import java.util.List;
public final class Example{
private final int uid;
private final List < Object > list;
private String fname;
public Example(int uid, List < Object > list, String fname) {
super();
this.uid = uid;
this.list = list;
this.fname = nfame;
}
public int getUId() {
return uid;
}
public String getName() {
return fname;
}
public List < Object > getList() {
return new LinkedList < > (list); // defensive
}
}
Now let’s give this code the value and change its value again to see if it’s giving the same output or not.
import java.util.LinkedList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List < Object > lst = new LinkedList < Object > ();
lst.add("hello");
lst.add("world");
Example e= new Example(46, lst, "Anurag");
System.out.println(e.getId());
System.out.println(e.getName());
System.out.println(e.getList());
//modify list
e.getList().add("James");
System.out.println(im.getList()); //remain unchanged
}
}
we will get the following output and the later given value i.e. James
will not be displayed.
46
Anurag
[hello, world]
[hello, world]
What happens when you don’t use deep copy and cloning?
When we don’t use deep copy and cloning in Java, we may encounter several problems. These problems can be related to object manipulation, data integrity, and unexpected behavior. There are some common consequences of not using deep copy and cloning:
- Shared references: If we assign an object to another variable or pass it as an argument to a method, both variables will reference the same object in memory. Any changes made to one variable will directly affect the other variable. It can lead to unintended modifications and potential data corruption.
- Inconsistent State: When using shallow copy, the copied object may end up with an inconsistent state, as not all properties are copied deeply. This can lead to unexpected behavior and bugs in your application.
- Security Risks: In scenarios where security is critical, a shallow copy can pose risks. If sensitive data is stored in objects and shared references allow unauthorized access to that data, it can lead to security breaches.
- Memory Management Issues: Shallow copying of objects containing shared references can lead to memory leaks. Garbage collection may not properly release the memory occupied by objects because their references are still in use.
- Nested object problems: In Java, objects often contain other objects or collections (e.g., arrays, lists). Shallow copying of such objects will only copy the references to the nested objects, not the objects themselves. As a result, modifications to nested objects in one copy will impact all other copies sharing the same reference.