00:00

How to Create an Immutable Class in Java

Last updated:

In this tutorial, we are going to discuss what immutable objects are and how to create a custom immutable class in Java.

In Java, an immutable object is one whose state cannot be changed once it has been created. To get a different value, you create a new object with the intended changes. Immutable classes are useful for representing values that should not change after they are initialized.

Note: Java provides many built-in immutable classes, such as String, the wrapper classes (Integer, Long, Float, etc.), BigInteger, BigDecimal, Locale and the java.time classes such as LocalDate.

1. Rules for Creating an Immutable Class

To create an immutable class in Java, follow these steps:

  1. Declare the class as final so no other class can extend it.
  2. Declare all data members private so they cannot be accessed directly from outside the class.
  3. Declare all data members final so their values cannot change after the object is created.
  4. Initialize all fields in a parameterized constructor, and make a deep (defensive) copy of any mutable object passed in, so the caller cannot change it later through its own reference.
  5. Provide only getter methods. For mutable fields, the getter should return a copy instead of the actual object reference.
  6. Do not provide setter methods, because setters change an object's state.

2. Example: A Custom Immutable Class

Let's understand a custom immutable class with the help of the example below. All fields of Student are String or int, which are already immutable, so no copies are needed.

ImmutableExample.java
// Declare class as final so it cannot be extended
final class Student {
    // Declare data members as private and final
    private final String studentName;
    private final String className;
    private final int rollNo;

    // Parameterized constructor initializes all fields once
    public Student(String studentName, String className, int rollNo) {
        this.studentName = studentName;
        this.className = className;
        this.rollNo = rollNo;
    }

    // Getter methods to access data outside the class
    // No setter methods are provided
    public String getStudentName() {
        return studentName;
    }

    public String getClassName() {
        return className;
    }

    public int getRollNo() {
        return rollNo;
    }
}

public class ImmutableExample {
    public static void main(String[] args) {
        // Create an object of the immutable class Student
        Student stuObject = new Student("Shreyansh Shanu", "Nursery", 11);
        System.out.println("Student Name: " + stuObject.getStudentName());
        System.out.println("Student Class: " + stuObject.getClassName());
        System.out.println("Student Roll No.: " + stuObject.getRollNo());
    }
}
Output
Student Name: Shreyansh Shanu
Student Class: Nursery
Student Roll No.: 11

3. Immutable Class with Mutable Fields

If a field refers to a mutable object such as a List, Date or array, marking it final is not enough: final only stops the reference from changing, not the object it points to. Make a copy in the constructor and in the getter, so no outside code ever holds a reference to the internal object.

DefensiveCopyExample.java
import java.util.ArrayList;
import java.util.List;

final class Employee {
    private final String name;
    private final List<String> skills; // List is mutable

    public Employee(String name, List<String> skills) {
        this.name = name;
        // Defensive copy: later changes to the caller's list do not affect this object
        this.skills = new ArrayList<>(skills);
    }

    public String getName() {
        return name;
    }

    public List<String> getSkills() {
        // Return a copy so callers cannot modify the internal list
        return new ArrayList<>(skills);
    }
}

public class DefensiveCopyExample {
    public static void main(String[] args) {
        List<String> skills = new ArrayList<>();
        skills.add("Java");
        skills.add("SQL");

        Employee emp = new Employee("Asha", skills);

        // Try to change the employee's skills from outside
        skills.add("Python");
        emp.getSkills().add("Go");

        System.out.println("Caller's list: " + skills);
        System.out.println("Employee skills: " + emp.getSkills());
    }
}
Output
Caller's list: [Java, SQL, Python]
Employee skills: [Java, SQL]

Changes made to the caller's list and to the list returned by getSkills() do not affect the Employee object.

4. Immutable Classes with Records (Java 16+)

A record is a compact way to declare an immutable data class. A record is implicitly final, its fields are private final, and it has no setters. Java also generates the constructor, the accessor methods, equals(), hashCode() and toString(). Mutable components still need a defensive copy, which you can make in a compact constructor.

RecordExample.java
import java.util.List;

// A record is final, its fields are private and final, and it has no setters
record Product(String name, double price, List<String> tags) {
    // Compact constructor: make an unmodifiable copy of the mutable list
    Product {
        tags = List.copyOf(tags);
    }
}

public class RecordExample {
    public static void main(String[] args) {
        Product p = new Product("Laptop", 899.99, List.of("electronics", "computers"));
        System.out.println(p);
        System.out.println("Name: " + p.name() + ", Price: " + p.price());

        try {
            p.tags().add("sale");
        } catch (UnsupportedOperationException e) {
            System.out.println("Cannot modify tags: " + e.getClass().getSimpleName());
        }
    }
}
Output
Product[name=Laptop, price=899.99, tags=[electronics, computers]]
Name: Laptop, Price: 899.99
Cannot modify tags: UnsupportedOperationException

5. Benefits of Immutable Classes

  • Thread safety: an immutable object cannot change state, so it cannot be corrupted or seen in an inconsistent state by concurrent threads. It can be shared without synchronization.
  • Safe as map keys and set elements: the hashCode() never changes, so the object is always found in a HashMap or HashSet.
  • Easy to cache and reuse: one instance can be shared everywhere, as Java does with the string pool.
  • Simpler code: once an object is created and valid, it stays valid, so there are fewer bugs caused by unexpected changes.

6. Summary

  • An immutable object's state cannot change after it is created.
  • Make the class final, the fields private final, and provide no setters.
  • Copy mutable objects in the constructor and in getters.
  • From Java 16, records are the shortest way to write an immutable data class.