Open In App

How to Create Immutable Class in Java?

Last Updated : 30 May, 2025
Comments
Improve
Suggest changes
Like Article
Like
Report

In Java, immutability means that once an object is created, its internal state cannot be changed. Immutable classes in Java provide many advantages like thread safety, easy debugging and all. In Java, all the wrapper classes (like Integer, Boolean, Byte, Short) and the String class is immutable. We can create our own immutable class as well.

In this article, we are going to learn:

  • What immutability means
  • Why it is useful
  • How to create our own immutable class
  • Why deep copying is important
  • What are the limitations Java record types have

What is an Immutable Class?

An immutable class is a class whose objects cannot be changed once created. If we do any modification, it results in a new object. This method is used in concurrent applications.

Rules for Creating an Immutable Class

  • The class must be declared as final so that child classes cannot be created.
  • Data members in the class must be declared private so that direct access is not allowed.
  • Data members in the class must be declared as final so that we can’t change their value after object creation.
  • A parameterized constructor should initialize all the fields, performing a deep copy so that data members can't be modified with an object reference.
  • Deep Copy of objects should be performed in the getter methods to return a copy rather than returning the actual object reference.

Note: There should be no setters or in simpler terms, there should be no option to change the value of the instance variable.


Example: Immutable class implementation

Student.java

Java
// Java Program to Create An Immutable Class
import java.util.HashMap;
import java.util.Map;

// declare the class as final
final class Student {

    // make fields private and final
    private final String name;
    private final int regNo;
    private final Map<String, String> metadata;

    // initialize all fields via constructor
    public Student(String name, int regNo, Map<String, String> metadata) {
        this.name = name;
        this.regNo = regNo;

        // deep copy of mutable object (Map)
        Map<String, String> tempMap = new HashMap<>();
        for (Map.Entry<String, String> entry : metadata.entrySet()) {
            tempMap.put(entry.getKey(), entry.getValue());
        }
        this.metadata = tempMap;
    }

    // only provide getters (no setters)
    public String getName() {
        return name;
    }

    public int getRegNo() {
        return regNo;
    }

    // return deep copy to avoid exposing internal state
    public Map<String, String> getMetadata() {
        Map<String, String> tempMap = new HashMap<>();
        for (Map.Entry<String, String> entry : this.metadata.entrySet()) {
            tempMap.put(entry.getKey(), entry.getValue());
        }
        return tempMap;
    }
}

In this example, we have created a final class named Student. It has three final data members, a parameterized constructor, and getter methods. Please note that there is no setter method here. Also, note that we don't need to perform deep copy or cloning of data members of wrapper types as they are already immutable.

Geeks.java:

Java
import java.util.HashMap;
import java.util.Map;

public class Geeks {
    public static void main(String[] args) {

        // create a map and adding data
        Map<String, String> map = new HashMap<>();
        map.put("1", "first");
        map.put("2", "second");

        // create an immutable Student object
        Student s = new Student("GFG", 101, map);

        // accessing data
        System.out.println(s.getName());       
        System.out.println(s.getRegNo());     
        System.out.println(s.getMetadata());   

        // try to modify the original map
        map.put("3", "third");
        System.out.println(s.getMetadata());   

        // try to modify the map returned by getMetadata()
        s.getMetadata().put("4", "fourth");
        System.out.println(s.getMetadata());   
    }
}

Even after modifying the original or returned Map, the internal state of the Student object remains unchanged. This confirms the immutability concept.

Output:

GFG
101
{1=first, 2=second}
{1=first, 2=second}
{1=first, 2=second}


Limitation of Java record with Mutable Fields

Java 14 introduced record. This is a clear and concise way to define immutable like classes:

record Student(String name, int regNo, Map<String, String> metadata) {}


But this only offers shallow immutability. If the Map is modified externally, the internal state of the record changes:

Map<String, String> map = new HashMap<>();

map.put("1", "first");


Student s = new Student("ABC", 101, map);


// Changes internal state — NOT safe

map.put("2", "second");

s.metadata().put("3", "third");

Note: Use record only if all fields are immutable types like String, int, or other records.



Next Article
Article Tags :
Practice Tags :

Similar Reads