An immutable object is an object whose state cannot be changed after the object has been created. Once its initial values are established, those values remain fixed for the lifetime of that object.
Immutability is a powerful design technique in Java because it makes objects easier to understand, safer to share, and less prone to unexpected changes. Instead of modifying an existing object, code creates a new object when a different state is required.
An immutable object does not change its internal state after construction. If a different value is required, a new object is created instead.
Why Do Immutable Objects Exist?
Mutable objects can be changed by any code that has permission to modify them. In a large application, this can make debugging surprisingly difficult because a value may change far away from the code where it was originally created.
Immutable objects remove that particular problem. Once created, their state is stable.
Think of a printed certificate. Once it has been issued, you do not erase the original certificate every time some information needs to change. You issue a new certificate. Immutable objects follow a similar idea.
Simple Immutable Object Example
final class Student { private final String name; private final int age; public Student(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public int getAge() { return age; } }
This class is designed to be immutable. Its fields are private and final, values are assigned through the constructor, and there are no setters that could change the state later.
Why Use final Fields?
The final keyword prevents a field from being assigned another value after its initial assignment.
class Product { private final String productCode; public Product(String productCode) { this.productCode = productCode; } }
After the constructor assigns productCode, the field cannot be assigned again.
final prevents reassignment of a field reference or primitive value. It does not automatically make the referenced object immutable.
Why Should Immutable Fields Be private?
Private fields prevent external classes from directly accessing the object's internal state. This supports encapsulation and allows the class to control how its state is exposed.
final class Account { private final String accountNumber; public Account(String accountNumber) { this.accountNumber = accountNumber; } public String getAccountNumber() { return accountNumber; } }
The account number can be read through the getter, but external code cannot directly replace the stored value.
Why Should Immutable Classes Avoid Setters?
A setter changes an object's state. Therefore, adding ordinary setters to an immutable class would defeat the purpose of immutability.
final class Employee { private final String name; public Employee(String name) { this.name = name; } public String getName() { return name; } // No setter }
If the employee name needs to be different, the design can create another Employee object rather than modifying the existing one.
Creating a New Object Instead of Modifying One
A useful pattern for immutable classes is to provide a method that creates a new object containing the desired change.
final class User { private final String name; private final int age; public User(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public int getAge() { return age; } public User withAge(int newAge) { return new User(name, newAge); } }
Calling withAge() does not modify the original User. It creates another User with the new age.
User first = new User("Amit", 20); User second = first.withAge(21); System.out.println(first.getAge()); System.out.println(second.getAge());
The first object still contains 20, while the second object contains 21. This is a key characteristic of immutable design.
Making a Class final
An immutable class is often declared final so that another class cannot extend it and introduce mutable behaviour.
final class Configuration { private final String environment; public Configuration(String environment) { this.environment = environment; } public String getEnvironment() { return environment; } }
Declaring the class final prevents subclassing. This removes one possible way for another class to introduce state-changing behaviour into the design.
Important: final Reference Does Not Mean Immutable Object
One of the most important distinctions in Java is that final protects the reference, not necessarily the object referred to by that reference.
class Team { private final List<String> members; public Team(List<String> members) { this.members = members; } public List<String> getMembers() { return members; } }
Although members is final, the List itself can still be changed. Code holding the list reference may add or remove elements.
Therefore, immutable design requires more than simply adding final to fields. Mutable referenced objects must also be handled carefully.
Defensive Copying
One technique for protecting mutable objects is defensive copying. Instead of storing the caller's mutable object directly, the class creates its own copy.
final class Team { private final List<String> members; public Team(List<String> members) { this.members = new ArrayList<>(members); } public List<String> getMembers() { return List.copyOf(members); } }
The constructor creates a separate list, and the getter returns an unmodifiable snapshot. This prevents callers from changing the Team's internal collection through the returned reference.
For immutable classes, always examine mutable fields such as collections, arrays, maps, and custom mutable objects. Simply declaring the reference final is not enough.
Immutable Objects and String
Java's String class is one of the best-known examples of an immutable object. Once a String object is created, its contents cannot be changed.
String first = "Hello"; String second = first.concat(" Java"); System.out.println(first); System.out.println(second);
The concat() operation does not modify first. Instead, it produces another String object containing the combined text.
Advantages of Immutable Objects
| Advantage | Why It Matters |
|---|---|
| Predictable state | The object's values cannot unexpectedly change after creation. |
| Safer sharing | Multiple parts of an application can safely reference the same immutable object. |
| Thread-friendly | Immutable state reduces the need for synchronization when objects are shared between threads. |
| Easier debugging | Once created, the object's state remains stable. |
| Reliable keys | Immutable objects can be safer candidates for use as keys when equality and hash code are stable. |
Immutable Objects and Thread Safety
Immutable objects are naturally easier to share between threads because one thread cannot change the object's state while another thread is reading it.
This does not mean every immutable object automatically solves every concurrency problem. However, removing mutable shared state eliminates an entire category of synchronization concerns.
If an object's state cannot change, other threads do not have to protect that state from ordinary mutations.
Common Beginner Mistakes
- Thinking final alone makes an object immutable.
- Providing setters in a class that is supposed to be immutable.
- Returning internal mutable collections directly from getters.
- Storing a caller-provided mutable object without making a defensive copy.
- Forgetting that objects referenced by final fields may still be mutable.
- Assuming immutable means no new objects can ever be created; immutable designs commonly create new objects for changed values.
Best Practices for Immutable Classes
- Declare the class final when subclassing could compromise the intended design.
- Keep instance fields private.
- Make fields final whenever their values are fixed after construction.
- Initialize all required state through constructors or controlled factory methods.
- Do not provide setters that mutate the object's state.
- Use defensive copies or immutable collections for mutable referenced data.
- Return safe representations of mutable internal objects.
Interview Insights
A common interview question is: “How do you create an immutable class in Java?” A strong answer should mention private fields, final state, initialization through a constructor, no setters, careful handling of mutable fields, and often a final class to prevent subclass-based mutation.
Another important interview question is: “Does final make an object immutable?” No. A final reference cannot point to another object, but the referenced object itself may still be modified if it is mutable.
Quick Revision
| Rule | Purpose |
|---|---|
| Private fields | Prevent direct external modification. |
| Final fields | Prevent reassignment after initialization. |
| No setters | Prevent ordinary state-changing methods from outside the class. |
| Final class | Prevents subclasses from extending the immutable design. |
| Defensive copies | Protect internal state from mutable external objects. |
| New object for changes | Preserves the original object's state while representing a new state. |
Immutable objects are one of the cleanest ways to make Java code predictable. By fixing an object's state after construction and carefully protecting referenced data, you reduce accidental changes, simplify reasoning, and make objects safer to share. The key lesson is that immutability is not achieved by adding final everywhere; it is a complete design decision about how an object's state is created, exposed, and protected.
