How to Create an Immutable Class in Java
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.
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:
- Declare the class as
finalso no other class can extend it. - Declare all data members
privateso they cannot be accessed directly from outside the class. - Declare all data members
finalso their values cannot change after the object is created. - 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.
- Provide only getter methods. For mutable fields, the getter should return a copy instead of the actual object reference.
- 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.
// 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());
}
}
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.
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());
}
}
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.
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());
}
}
}
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 aHashMaporHashSet. - 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 fieldsprivate 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.
Frequently Asked Questions (FAQ)
Q1. What is an immutable class in Java?
A class whose objects cannot change state after they are created. String and Integer are examples.
Q2. Why should an immutable class be final?
If the class is not final, a subclass could add mutable state or override methods, so code that expects an immutable object could receive a mutable one.
Q3. Is declaring all fields final enough to make a class immutable?
No. final only stops the reference from changing. If a field points to a mutable object such as a List, its contents can still change, so you also need defensive copies.
Q4. What is a defensive copy?
A copy of a mutable object made in the constructor or getter, so that outside code never holds a reference to the object stored inside the immutable class.
Q5. Why is String immutable in Java?
For security (file paths, URLs and class names cannot be changed after checks), for the string pool (one literal can be shared safely), for thread safety, and so its hash code can be cached.
Q6. Are immutable objects thread-safe?
Yes. Their state never changes, so many threads can read them at the same time without synchronization.
Q7. Is a Java record immutable?
A record is shallowly immutable: it is final and its fields are private final. If a component is mutable, such as a List, copy it in the compact constructor, for example with List.copyOf.
Q8. How do you change a value of an immutable object?
You create a new object with the changed value. For example, String.toUpperCase() and LocalDate.plusDays() return new objects and leave the original unchanged.
Q9. What is the difference between immutable and unmodifiable collections?
An unmodifiable view, such as Collections.unmodifiableList(list), blocks changes through the view but still reflects changes made to the original list. List.copyOf and List.of create collections that cannot change at all.
Q10. Can an immutable class have a static factory method instead of a public constructor?
Yes. A private constructor with a factory method such as of(...) is common. It lets the class cache and reuse instances, as Integer.valueOf does.