CoreJava Java

How to Create Immutable Classes in Java – Best Practices with Examples

How to Create Immutable Classes in Java – Best Practices with Examples

An immutable class is a class whose instances cannot be changed after they are created. Once you construct an immutable object, its fields stay the same for its entire lifetime. String, Integer, BigDecimal, and LocalDate in the Java standard library are all immutable classes.

Immutable objects are inherently thread-safe, easier to reason about, and make excellent keys in HashMap and HashSet. This tutorial shows you exactly how to create one.

Rules for Making a Class Immutable

  1. Declare the class final — prevents subclasses from overriding behavior
  2. Declare all fields private final — prevents direct access and reassignment
  3. No setter methods — only provide getters
  4. Deep copy mutable fields in the constructor — prevent external mutation through shared references
  5. Return defensive copies of mutable fields from getters

Simple Immutable Class


package com.java9r;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * ImmutableEmployee – a thread-safe, immutable value object.
 * Once created, no field can be changed.
 */
public final class ImmutableEmployee {

    private final int    id;
    private final String name;
    private final double salary;
    private final List<String> skills;  // mutable field – needs defensive copy

    public ImmutableEmployee(int id, String name, double salary, List<String> skills) {
        this.id     = id;
        this.name   = name;
        this.salary = salary;
        // Defensive copy: don't store the caller's list reference
        this.skills = Collections.unmodifiableList(new ArrayList<>(skills));
    }

    // Only getters – no setters
    public int    getId()     { return id; }
    public String getName()   { return name; }
    public double getSalary() { return salary; }

    // Return unmodifiable view – caller cannot modify
    public List<String> getSkills() { return skills; }

    @Override
    public String toString() {
        return String.format("Employee{id=%d, name='%s', salary=%.2f, skills=%s}",
                             id, name, salary, skills);
    }
}

Test the Immutable Class


package com.java9r;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class ImmutableDemo {

    public static void main(String[] args) {

        List<String> skillList = new ArrayList<>(Arrays.asList("Java", "Spring", "MySQL"));

        ImmutableEmployee emp = new ImmutableEmployee(101, "Ravi Kumar", 75000.0, skillList);
        System.out.println("Created: " + emp);

        // Try to change the original list – should NOT affect emp
        skillList.add("Hacking attempt");
        System.out.println("After modifying original list: " + emp);
        // skills list in emp is unchanged – defensive copy worked

        // Try to modify the returned skills list – throws UnsupportedOperationException
        try {
            emp.getSkills().add("Another hack");
        } catch (UnsupportedOperationException e) {
            System.out.println("Cannot modify returned skills: " + e.getClass().getSimpleName());
        }

        // Use as HashMap key – safe because hash never changes
        var map = new java.util.HashMap<ImmutableEmployee, String>();
        map.put(emp, "Engineering");
        System.out.println("Department: " + map.get(emp));
    }
}

Expected Output


Created: Employee{id=101, name='Ravi Kumar', salary=75000.00, skills=[Java, Spring, MySQL]}
After modifying original list: Employee{id=101, name='Ravi Kumar', salary=75000.00, skills=[Java, Spring, MySQL]}
Cannot modify returned skills: UnsupportedOperationException
Department: Engineering

Record Classes (Java 16+) — Immutability Made Easy

Java 16 introduced record classes that are immutable by default with minimal boilerplate:


// Java 16+ record – immutable, auto-generates constructor, getters, equals, hashCode, toString
record Point(double x, double y) {}

var origin = new Point(0.0, 0.0);
var p1     = new Point(3.0, 4.0);
System.out.println(p1.x());  // 3.0
System.out.println(p1.y());  // 4.0
// p1.x = 5.0; // compile error – records are immutable

Benefits of Immutable Classes

  • Thread safety: No synchronization needed — multiple threads can read simultaneously
  • Safe as Map/Set keys: Hash code never changes, so keys don't "disappear" from collections
  • Easier debugging: State never changes after construction — no surprise mutations
  • Free to share: No need to defensive-copy when passing to methods

Summary

To create an immutable class: mark it final, make all fields private final, initialize them only in the constructor, provide no setters, and use defensive copies for any mutable fields like lists, dates, or arrays. Immutable objects are the easiest path to thread-safe code and are a core pattern in functional and concurrent Java programming. In Java 16+, record types give you immutability automatically.

Topics: CoreJava Java
← Newer Post Older Post →