How to create an Immutable class using Record in Java
Extensive guide for learning Immutable classes and objects in Java.
Table of Contents
- Introduction to Immutability in Java.
- Implement Immutable class using Final and Private keywords.
- Why We Need the Record in Java?
- Model Immutable data using Record.
- Key points.
Introduction to Immutability in Java
Immutability refers to the capability of something not to change once it is initialized.
In technical words, we can say In object-oriented and functional programming, an immutable object is an object whose state (data member and method) cannot be modified after it is created/initialized. This is in contrast to a mutable object, which can be modified after it is created or initialized.
For example — All primitive wrapper classes (Integer, Long, Byte, Short, Double, Float, Boolean, and Character) and String classes are Immutable in Java, hence they can’t be modified once they are initialized usually they create another object, and store the new value in the new object.
Implement Immutable class using Final and Private keywords.
Immutability means there is no way to change the state of the object once it is initialized/created and this can be achieved by these three rules.
- All non-private fields should be final or All the fields should be Private. Note — It is recommended to use both fields private and final.
- There should not be any method in the class that changes the fields of objects in any situation.
- Any object references defined in the class shouldn’t be modified outside the class — This can be achieved with the help of Defensive copy.
// Here class should be final or non-final? it's also a big debate.
final public class Immutable {
private final String name ;
private final Date date ;
private final int age ;
public Immutable(String Name, int Age, Date Date){
this.name = Name ;
this.age = Age ;
this.date = new Date(Date.getTime()) ; // Defensive copy in constructor
}
public String getProfile(){
return name + " is of " + age + " years old" ;
}
@Override
public String toString(){
return "Immutable{date=" + date + "}" ;
}
public Date getDate(){
return new Date(date.getTime()) ; // Defensive copy in the getter.
}
@Override
public boolean equals(Object obj) {
return true ;//test
}
@Override
public int hashCode() {
return 3; //test
}
}
// Test the class
public class Main {
public static void main(String[] args) {
String name = "Unknown Person";
int age = 00;
Date initialDate = new Date() ;
// Create an immutable class object.
Immutable immutable = new Immutable(name, age, initialDate) ;
System.out.println("UnModified Profile " + immutable.getProfile()) ;
name = "Unknown_Person" ;
// Remains unchanged, we set the value in the constructor.
System.out.println("Immutable profile after modifying the name " + immutable.getProfile());
System.out.println("\n==============================\n");
System.out.println("Initial date: " + initialDate);
System.out.println("Immutable date: " + immutable.getDate());
// Modify original date object.
initialDate.setTime(initialDate.getTime()+1000000000);
System.out.println("Modified initial date: " + initialDate);
// Check again to ensure that the internal date in immutable object remains unchanged.
// This is protected due to the constructor defensive copy.
System.out.println("Immutable date after modifying the initial date " + immutable.getDate());
// Try to modify the obtained date from the immutable object.
Date obtained = immutable.getDate() ;
obtained.setTime(obtained.getTime()+100000000);
System.out.println("Modified obtained date: " + obtained);
// Check again to ensure that the internal date in immutable object remains unchanged.
// This is protected due to the getter method Defensive copy.
System.out.println("Immutable date after modifying obtained date from object: " + immutable.getDate() );
}
}Let’s break down the above code, don't panic because of the bigger code, most are just test cases, and real code is of just a few lines only.
- Initial Date: This is the original date object created.
- Immutable Date: This is the date stored in the ‘Immutable’ class object.
- Modified Initial Date: Confirms that the initial date has been modified.
- Immutable Date after modifying the initial date: Confirms that the date inside the ‘immutable’ has not been changed even after modifying the initial date.
- Modified Obtained Date: The obtained date from the ‘Immutable’ object has been modified.
- Immutable date after modifying obtained date from object: Confirms that the Internal date in the ‘Immutable’ remains unchanged after modifying the obtained date.
Why We Need the Record in Java?
If you need to carry the instances of this class from one application to another, either by sending them over a network or through a file system, you may also consider making this class serializable. Then you may need to add some information on how the instances of this class are serialized.
Get Abhishek Kumar’s stories in your inbox
Join Medium for free to get updates from this writer.
In the end, your Immutable class may be a thousand lines long, mostly populated with code generated by your IDE, just to model an immutable aggregation of name, age, and Date that you need to write to a file.
Model Immutable data using Record
Records have been added to the JDK in Java SE 14 to change this. The record gives you all this with a single line of code. All you need to do is declare the state of a record; the rest is generated for you by the compiler.
public record Immutable(String name, int age, Date date) {}This single line of code creates the following elements for the record named ‘Immutable’.
- It is an immutable class with these fields:
name:String,age:int,date:Date. - It has a canonical constructor(compiler creates a constructor of its own), which takes the components of your record as arguments and copies their values to the fields of the record class.
- The
toString(),equals()andhashCode(), the compiler creates these with the default behavior, You can override these. - It can implement
Serializableinterface so that you can send instances ofImmutableclass to other applications over a network or file system.
(String name, int age, Date date)- It declares the components(name, age, & date) of the record. For each component of a record, the compiler creates a private final field and an accessor with the same name as the component.
private final String name ;
private final Date date ;
private final int age ;
public String name() {
return this.name;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
// If this works for you application its well and good else you may need to
// implement your own accessor for example if you need to return defensive
// copy for some of particular component.Defining multiple constructors
We can define any number of constructors as long as they call the canonical constructor of the record.
public record State(String name, String capitalCity, List<String> cities) {
public State {
// List.copyOf returns an unmodifiable copy,
// so the list assigned to `cities` can't change anymore
cities = List.copyOf(cities);
}
public State(String name, String capitalCity) {
this(name, capitalCity, List.of());
}
public State(String name, String capitalCity, String... cities) {
this(name, capitalCity, List.of(cities));
}
}Things you Cannot Add to a Record
There are three things that you cannot add to a record:
- You cannot declare any instance field in a record. You cannot add any instance field that would not correspond to a component.
- You cannot define any field initializer.
- You cannot add any instance initializer.
You can create static fields with initializers and static initializers.
Key Points
- While implementing the Immutable class we should consider using a defensive copy for getter and constructor.
- Defensive copy is a programming technique used to protect data from being modified by creating a copy of the original data.
- We create the immutable class using private and final keywords which ends with lots of code just for a small goal, here record does in one line.
- Records are introduced in Java SE 14.
- Syntax of record —
public record className(type arg1, type arg2, .) - The compiler creates a constructor of its own called Canonical constructor.
- The component of the record is used as the argument for the canonical constructor.
- You can also add any constructor to a record, as long as this constructor calls the canonical constructor of your record.
- You may consider overriding the component default value in case you need to validate the state of your record or while making a defensive copy of a mutable component.
- You can’t define any field or instance initializer.
- We can implement our accessor by overriding the default one.
Thank you for reading this article till the end, I hope it added some value to your skills. Please do consider liking and Sharing your feedbacks.






