Immutable Class

An immutable class in Java is a class whose objects cannot be modified after they are created. Once an instance of an immutable class is initialized, its state remains unchanged throughout its lifetime. Immutable classes are useful for ensuring thread safety and preventing unintended modifications.

Key Characteristics of Immutable Classes
  1. Final Class – The class is declared as final to prevent subclassing.
  2. Final Fields – All fields are declared private and final so they can’t be modified after object creation.
  3. No Setter Methods – There are no methods that allow changing the fields after initialization.
  4. Constructor Initialization – All fields are initialized through the constructor.
  5. Deep Copies – If the class has mutable objects, it returns defensive copies instead of direct references.
Example of an Immutable Class
						final class ImmutablePerson {
    private final String name;
    private final int age;

    // Constructor
    public ImmutablePerson(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // Getter methods (No setters)
    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}
					

Why Use Immutable Classes?

  • Thread Safety – Immutable objects can be safely shared across multiple threads without synchronization.
  • Caching & Performance – They can be reused since their state doesn’t change.
  • Security – Immutable classes prevent unintended modifications, making them more predictable and reliable.

Java has built-in immutable classes like String , Integer , and BigDecimal . If you need any more details, let me know!

Steps to Create an Immutable Class
  1. Declare the Class as final
    • This prevents direct access and ensures fields cannot be modified after initialization.
  2. Make All Fields private and final
    • Prevents direct access and ensures fields cannot be modified after initialization.
  3. Initialize Fields Through a Constructor
    • Assign values only once when the object is created.
  4. Do Not Provide Setter Methods
    • Setters allow modification, which breaks immutability.
  5. Return Defensive Copies of Mutable Objects (if any)
    • If your class has fields that are references to mutable objects, return a new copy instead of the original.
Example of an Immutable Class
						final class ImmutableStudent {
    private final String name;
    private final int age;
    private final List<String> subjects;

    // Constructor
    public ImmutableStudent(String name, int age, List<String> subjects) {
        this.name = name;
        this.age = age;
        // Defensive copy to prevent external modification
        this.subjects = new ArrayList<>(subjects);
    }

    // Getter methods (No setters)
    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public List<String> getSubjects() {
        // Returning defensive copy to prevent modification
        return new ArrayList<>(subjects);
    }
}
				

Benefits of Immutable Classes

  • Thread Safety – No synchronization needed since the state doesn’t change.
  • Reliable Hashing – Perfect for use in collections like HashMap and HashSet.
  • Security – Prevents accidental or malicious modifications.
  • Simplified Code No need to handle state changes.
						final class Student {
    private final String name;
    private final Address address; // Contains an immutable deep copy.

    // Constructor with Deep Copy for Address
    public Student(String name, Address address) {
        this.name = name;
        this.address = new Address(address.getCity()); // Deep Copy for immutability
    }

    // Getter methods (No Setters)
    public String getName() {
        return name;
    }

    public Address getAddress() {
        return new Address(address.getCity()); // Defensive Copy
    }

    @Override
    public String toString() {
        return "Student{name='" + name + "', address=" + address.getCity() + "}";
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (!(obj instanceof Student)) return false;
        Student other = (Student) obj;
        return name.equals(other.name) && address.getCity().equals(other.address.getCity());
    }

    @Override
    public int hashCode() {
        return name.hashCode() + address.getCity().hashCode();
    }
}

final class Address {
    private final String city;

    public Address(String city) {
        this.city = city;
    }

    public String getCity() {
        return city;
    }

    @Override
    public String toString() {
        return "Address{city='" + city + "'}";
    }
}
					

In Java, deep copy and shallow copy care two ways of copying objects, and they differ in how they handle references to nested or contained objects.