Immutable Objects in OOP – How to Design and Use in Java

Illustration for Immutable Objects in OOP – How to Design and Use in Java
By Last updated:

Introduction

In Java Object-Oriented Programming (OOP), immutability is a powerful design principle that promotes thread-safety, predictability, and simplicity. Immutable objects are those whose state cannot change after construction. Understanding and applying this concept is crucial for writing robust and maintainable code.

What Are Immutable Objects?

An immutable object is one whose internal state remains constant after it is fully constructed. Once initialized, it cannot be modified.

Real-World Analogy

Think of a sealed letter – once you've written it and sealed the envelope, the contents inside cannot be altered. This immutability ensures consistency and safety.

Benefits of Immutability in Java

  • Thread-safety: No need for synchronization.
  • Predictability: Data does not change unexpectedly.
  • Safe sharing: Immutable objects can be shared freely between classes.
  • Better caching and optimization.

How to Design Immutable Objects in Java

To make a class immutable:

  1. Declare the class as final so it can't be subclassed.
  2. Make all fields private and final.
  3. Don’t provide setters.
  4. Initialize all fields via constructor.
  5. If a field is a mutable object, make a defensive copy.

Example: Immutable User Class

public 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;
    }
}

Using record (Java 14+)

Java records simplify immutable object creation.

public record User(String name, int age) {}

This single line provides all constructor, getters, equals, hashCode, and toString.

UML Representation

+-----------------+
|     User        |
+-----------------+
| - name: String  |
| - age: int      |
+-----------------+
| +getName(): String |
| +getAge(): int     |
+-----------------+

Common Pitfalls and Fixes

Pitfall 1: Mutable fields not handled

private final List<String> tags;

Fix: Use defensive copies.

this.tags = new ArrayList<>(tags);

Pitfall 2: Allowing mutation through returned references

Always return unmodifiable views or copies of collections.

public List<String> getTags() {
    return Collections.unmodifiableList(tags);
}

When to Use Immutable Objects

  • As value objects (e.g., Money, Name, Email)
  • DTOs (Data Transfer Objects)
  • For shared objects in concurrent apps
  • In cache keys

Drawbacks of Immutability

  • Can cause object creation overhead due to lack of setters
  • Can be verbose in older Java versions (before records)

Best Practices

  • Prefer record for simple value classes (Java 14+)
  • Avoid exposing internal mutable state
  • Document immutability in class-level Javadoc
  • Use builder pattern when many fields are involved

Refactoring Example

Before (Mutable)

public class Product {
    private String name;
    private double price;

    public void setName(String name) { this.name = name; }
    public void setPrice(double price) { this.price = price; }
}

After (Immutable)

public record Product(String name, double price) {}

Conclusion

Immutability in Java leads to simpler, safer, and more predictable applications. By enforcing unchangeable state, developers reduce bugs, simplify testing, and improve code modularity.

Key Takeaways

  • Immutable objects cannot be modified after construction.
  • They promote thread-safety, readability, and maintainability.
  • Java record is the modern way to create simple immutable data holders.

FAQ

1. Can an object be partially immutable?

No. If even one field is mutable, the object is not truly immutable.

2. Are Java String objects immutable?

Yes. String is a classic example of an immutable class in Java.

3. Why make objects immutable?

To prevent accidental changes, enhance thread-safety, and support functional programming.

4. How do I handle optional fields in an immutable class?

Use the Builder pattern or Java Optional.

5. Can I serialize immutable objects?

Yes. Implement Serializable or annotate records with @Serializable in custom frameworks.

6. Is using record better than class?

For simple value types, yes. But record doesn’t allow mutable logic or behavior.

7. Can an immutable object have mutable internal references?

Yes, but they must be handled with defensive copying to maintain immutability.

8. Does immutability make objects thread-safe?

Yes. Since state doesn’t change, they are inherently thread-safe.

9. Are all final classes immutable?

No. final prevents subclassing but does not guarantee field immutability.

10. Can I make legacy mutable classes immutable?

Yes, by refactoring to remove setters and using constructors for initialization.