Immutable classes in Java

Oct 2, 2022·

5 min read

“Why Strings are designed as immutable in Java?”

This is one of the famous question in Java Interviews . There are many reasons for designing string to be immutable.

  • if the key in a hash map is a string .and if the key is modified then it will through Null Pointer exception

Example :

String s1="sunil";
hm.put(s1,100);
s1.toUpperCase();
hm.get(s1);  // below code will through NPE if the string is mutable
  • in Java , String constant pool area is the special memory region where Strings are stored by the JVM. Since Strings are immutable in Java, the JVM optimizes the amount of memory allocated for them by storing only one copy of each literal String in the pool. This process is called interning:
  • Strings provides a good security . Hence we will use strings to store a sensitive pieces of information like username, passwords , Connection URLs etc..

  • As the strings are immutable. they will be used in many places in multi-threading. accessing from multiple threads won't change value of the strings as they were immutable

  • Immutability improves the performance by saving memory in heap space and faster access of hash implementations when operated with Strings.

To Make the class immutable we need to follow certain steps :

  • Declare the class as final so that the class can’t be extended by the sub class

  • Make all fields private so that direct access is not allowed.

  • Make all mutable fields final so that its value can be assigned only once.

  • Don’t provide setter methods for variables. if we provide they will change the values of the fields

  • Initialize all the fields via a constructor performing deep copy.

  • Perform cloning of objects in the getter methods to return a copy rather than returning the actual object reference.

Constructor performing Shallow Copy

The default implementation of the Java Object clone() method is using shallow copy. It’s using reflection API to create the copy of the instance. The below code snippet showcases the shallow cloning implementation.

/**
     * Constructor performing Shallow Copy
     * @param i
     * @param n
     * @param hm
     */
    public ClassSunilTest(int i, String n, HashMap<String,String> hm){
        System.out.println("Performing Shallow Copy for Object initialization");
        this.id=i;
        this.name=n;
        this.testMap=hm;
    }

Constructor performing Deep Copy

In deep cloning, we have to copy fields one by one. If we have a field with nested objects such as List, Map, etc. then we have to write the code to copy them too one by one. That’s why it’s called deep cloning or deep copy. We can override the Employee clone method like the following code for deep cloning.

/**
     * Constructor performing Deep Copy
     * @param i
     * @param n
     * @param hm
     */

    public ClassSunilTest(int i, String n, HashMap<String,String> hm){
        System.out.println("Performing Deep Copy for Object initialization");
        this.id=i;
        this.name=n;
        HashMap<String,String> tempMap=new HashMap<String,String>();
        String key;
        Iterator<String> it = hm.keySet().iterator();
        while(it.hasNext()){
            key=it.next();
            tempMap.put(key, hm.get(key));
        }
        this.testMap=tempMap;
    }

How to create a Immutable class

we have taken above steps into consideration to create an Immutable class

import java.util.HashMap;
import java.util.Iterator;

public final class FinalClassExample {

    private final int id;

    private final String name;

    private final HashMap<String,String> testMap;

    public int getId() {
        return id;
    }


    public String getName() {
        return name;
    }

    /**
     * Accessor function for mutable objects
     */
    public HashMap<String, String> getTestMap() {
        //return testMap;
        return (HashMap<String, String>) testMap.clone();
    }

    /**
     * Constructor performing Deep Copy
     * @param i
     * @param n
     * @param hm
     */

    public FinalClassExample(int i, String n, HashMap<String,String> hm){
        System.out.println("Performing Deep Copy for Object initialization");
        this.id=i;
        this.name=n;
        HashMap<String,String> tempMap=new HashMap<String,String>();
        String key;
        Iterator<String> it = hm.keySet().iterator();
        while(it.hasNext()){
            key=it.next();
            tempMap.put(key, hm.get(key));
        }
        this.testMap=tempMap;
    }


    /**
     * Constructor performing Shallow Copy
     * @param i
     * @param n
     * @param hm
     */
    /**
    public FinalClassExample(int i, String n, HashMap<String,String> hm){
        System.out.println("Performing Shallow Copy for Object initialization");
        this.id=i;
        this.name=n;
        this.testMap=hm;
    }
    */

    /**
     * To test the consequences of Shallow Copy and how to avoid it with Deep Copy for creating immutable classes
     * @param args
     */
    public static void main(String[] args) {
        HashMap<String, String> h1 = new HashMap<String,String>();
        h1.put("1", "first");
        h1.put("2", "second");

        String s = "original";

        int i=10;

        FinalClassExample ce = new FinalClassExample(i,s,h1);

        //Lets see whether its copy by field or reference
        System.out.println(s==ce.getName());
        System.out.println(h1 == ce.getTestMap());
        //print the ce values
        System.out.println("ce id:"+ce.getId());
        System.out.println("ce name:"+ce.getName());
        System.out.println("ce testMap:"+ce.getTestMap());
        //change the local variable values
        i=20;
        s="modified";
        h1.put("3", "third");
        //print the values again
        System.out.println("ce id after local variable change:"+ce.getId());
        System.out.println("ce name after local variable change:"+ce.getName());
        System.out.println("ce testMap after local variable change:"+ce.getTestMap());

        HashMap<String, String> hmTest = ce.getTestMap();
        hmTest.put("4", "new");

        System.out.println("ce testMap after changing variable from accessor methods:"+ce.getTestMap());

    }

}

Output of the above example program is:

Performing Deep Copy for Object initialization
true
false
ce id:10
ce name:original
ce testMap:{2=second, 1=first}
ce id after local variable change:10
ce name after local variable change:original
ce testMap after local variable change:{2=second, 1=first}
ce testMap after changing variable from accessor methods:{2=second, 1=first}
Performing Shallow Copy for Object initialization
true
true
ce id:10
ce name:original
ce testMap:{2=second, 1=first}
ce id after local variable change:10
ce name after local variable change:original
ce testMap after local variable change:{3=third, 2=second, 1=first}
ce testMap after changing variable from accessor methods:{3=third, 2=second, 1=first, 4=new}