Core Java

Immutable Class Java Example

In this article, we will explain what Immutable Class is in Java through examples.

1. Introduction

In programming, an object is considered immutable if its state cannot change after it is created. Java not only supports immutable objects but as a best practice, it should be widely used. In this post, we will take a look at how to create immutable objects, their use cases, and some examples of immutable Classes.

The technologies that we will use in the code examples are:

  • Java 8
  • Eclipse 4.10.0

2. Final keyword

In Java, immutability can be achieved in fields and objects. By default, these are mutable, which means their state can change. To make them immutable we should use the final keyword when declaring them. When we use the final keyword on a field or object, then we must initialize it otherwise we will get a compilation error.

Below we see an example of an immutable object where its fields are declared final and class is declared final.

Color

final class Color {
    
    private final String name;
    private final String hex;
    
    public Color(String name, String hex) {
        this.name = name;
        this.hex = hex;
    }
    
    public String getName() {
        return name;
    }

    public String getHex() {
        return hex;
    }
}

The Color class has 2 fields that are immutable as they are declared final. These must be initialized through the constructor. In this class, we see that there are no setters as the state of the fields can’t be modified. Let’s create a new Color object and also make it final.

ImmutableExample

public class ImmutableExample {

    public static void main(String[] args) {
        final Color red = new Color("RED", "#ff0000");
        System.out.printf("Color %s hex is %s", red.getName(), red.getHex());
    }
}

In the main method, we create a new Color object and through the constructor, we pass the values for the fields. This object is also declared final which means that it can’t be initialized again as it can’t change. The output of this would be as expected.

Ouput

Color RED hex is #ff0000

3. Concurrent Applications

In a multi-threading environment, the state of an object can change by multiple threads, and as such, this will lead to an inconsistent state of the object. Immutable objects are very useful in concurrent applications since they cannot change state, they cannot be corrupted by thread interference or observed in an inconsistent state.

In the following example, we see how an object can change by many threads and have an inconsistent state.

ConcurrentExample

public class ConcurrentExample {

    public static void main(String[] args) throws InterruptedException {
        Number number = new Number(1);

        for (int i = 0; i < 5; i++) {
            Thread t = new NumberChangerThread(number);
            t.start();
        }

        Thread.sleep(1000);
    }
}

class NumberChangerThread extends Thread {

    private Number number;

    public NumberChangerThread(Number number) {
        this.number = number;
    }

    @Override
    public void run() {
        int random = new Random().nextInt(100);
        System.out.println("changing number to " + random);
        number = new Number(random);
        System.out.println("number changed to " + number.getId());
    }
}

class Number {

    private final int id;

    public Number(int id) {
        this.id = id;
    }

    public int getId() {
        return id;
    }
}

In the above code, we create a Number object which sets an initial value to its id field. Then multiple threads change the reference of this object by assigning new instances to it. Note that we can’t change the value of id as it is declared final and it’s immutable. The threads then set and get the id field which leads to an inconsistent state. Let’s see the output and verify that.

Ouput

changing id to 29
changing id to 47
changing id to 73
id changed to 73
changing id to 89
id changed to 89
changing id to 95
id changed to 95
id changed to 47
id changed to 29

From the above output, we confirm that the id field does not always have the same value when it is returned from the threads. This inconsistency is only possible for mutable objects. If we still wanted to use mutable objects, then we would have to wrap the set and get methods in a synchronized block.

4. Why String is Immutable?

A string is one of the most widely used classes in Java. It was designed to be immutable for performance and security purposes. Let’s see those in more detail.

4.1 Performance

The JVM stores the String literals in a special area in memory called the String Pool. Anytime a new String literal is created, then the JVM checks if it is already in the pool and it returns a reference to that object. That reduces the memory allocated by the Strings in a program.

Immutable Class java - Java String Pool
Java String Pool

4.2 Security

Immutability for Strings provides security for a number of use cases. For example, it is very common that we save passwords in Strings. If the String was mutable and the password could change then this would be a huge security concern. Another case is when we create an SQL where if the String was mutable the SQL could change and this would end up in the wrong statement or even in SQL injection.

Finally, as we saw in the previous section Strings are great when it comes to multi-threading applications as they are immutable and they cannot change state.

5. Immutable Collections

The java.util.Collections class provides convenient methods that make a Collection immutable. An immutable Collection cannot set, add, or remove any of its items. These methods are:

  • unmodifiableCollection(Collection)
  • unmodifiableList(List)
  • unmodifiableMap(Map)
  • unmodifiableNavigableMap(NavigableMap)
  • unmodifiableNavigableSet(NavigableSet)
  • unmodifiableSet(Set)
  • unmodifiableSortedMap(SortedMap)
  • unmodifiableSortedSet(SortedSet)

Let’s create an immutable ArrayList and try to add a new item in it.

ImmutableCollectionsExample

public class ImmutableCollectionsExample {

    public static void main(String[] args) {
        List list = new ArrayList();
        list.add(1);
        list.add(2);
        list = Collections.unmodifiableList(list);
        list.add(3);
    }
}

In the above example, we first create a new ArrayList, then we make it immutable and finally we add a new item. The final operation would throw an exception as the list is immutable.

Ouput

Exception in thread "main" java.lang.UnsupportedOperationException
	at java.util.Collections$UnmodifiableList.set(Collections.java:1311)
	at com.javacodegeeks.ImmutableCollectionsExample.main(ImmutableCollectionsExample.java:14)

The exception that was thrown was the UnsupportedOperationException as the add operation is not supported for immutable Collections.

6. Builder Pattern

The Builder pattern is a very common design pattern that provides a flexible solution to various object creation problems in object-oriented programming. The Builder pattern is very useful when it comes to creating immutable objects. Let’s see an example below.

Employee

class Employee {
    
    private final String name;
    private final String email;
    
    private Employee(EmployeeBuilder builder) {
        this.name = builder.name;
        this.email = builder.email;
    }
    
    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }
    
    static class EmployeeBuilder {
    
        private String name;
        private String email;
        
        public Employee build() {
            return new Employee(this);
        }
        
        public EmployeeBuilder setName(String name) {
            this.name = name;
            return this;
        }
        
        public EmployeeBuilder setEmail(String email) {
            this.email = email;
            return this;
        }
    }
}

Above, we create the immutable class Employee which has a private constructor and as such objects can’t be instantiated. For that, we embed the EmployeeBuilder class which has setters for the fields of the Employee and a build method that returns a new Employee object. Therefore, the Employee object cannot change as soon as it is created. Below we instantiate a new Employee object through the builder.

ImmutableBuilderExample

public class ImmutableBuilderExample {

    public static void main(String[] args) {
        Employee emp = new EmployeeBuilder().setName("John Smith").setEmail("johnsmith@example.com").build();
        System.out.printf("%s's email is %s", emp.getName(), emp.getEmail());
    }
}

The EmployeeBuilder class helps us set the fields of the Employee object and then return a new instance of it. The output of this would be:

Ouput

John Smith's email is johnsmith@example.com

7. Immutability in Sets & Maps

Immutable objects should be used in the java.util.Set and java.util.Map class. The Set class should contain immutable elements and the Map class should contain immutable keys.
If those are mutable, then the hashCode & equals methods will not work as expected. The following example demonstrates bad usage of a Set with mutable objects.

SetMutableExample

public class SetMutableExample {

    public static void main(String[] args) {
        HashSet numbers = new HashSet();
        Numbers n1 = new Numbers(1);
        Numbers n2 = new Numbers(2);
        Numbers n3 = new Numbers(3);
        numbers.add(n1);
        numbers.add(n2);
        numbers.add(n3);

        System.out.println("Numbers: " + numbers);
        System.out.println("Numbers contain 4: " + numbers.contains(new Numbers(4)));

        // change n1 id
        n1.setId(4);

        System.out.println("\nNumbers: " + numbers);
        System.out.println("Numbers contain 4: " + numbers.contains(new Numbers(4))); // wrong !!!
    }
}

class Numbers {

    private int id;

    public Numbers(int id) {
        this.id = id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public int getId() {
        return id;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + id;
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        Numbers other = (Numbers) obj;
        if (id != other.id) {
            return false;
        }
        return true;
    }

    @Override
    public String toString() {
        return id + "";
    }
}

In the code above, we create a HashSet (implementation of Set) in which we add 3 mutable Numbers objects. Then in line 16, we change the reference of one of the objects added, which also replaces the element in the set. This impacts the hashCode method which makes the contains method (line 19) return an incorrect result. Let’s see the output and confirm that.

Ouput

Numbers: [1, 2, 3]
Numbers contain 4: false

Numbers: [4, 2, 3]
Numbers contain 4: false

From the output above we see that the line 5 has incorrect result as the set does have this number in it.

8. Maintain Immutability when Field is Mutable

Immutable class means its states cannot change after the creation. There are two steps that must be done to maintain the immutability when fields are mutable.

  • Declare the mutable fields as final.
  • Create a new copy of the object from a passing mutable object in a constructor.
  • Stop any reference to any externally mutable object. It means getters should return a copy of the mutable instance.

Let’s create an ImmutableHasMutableField class which has a mutable object. Please pay attention to the constructor and getMutablePOJO method. Both have no reference to any external object. The getMutablePOJO method returns a cloned copy of the MutablePOJO object. Therefore the immutability is maintained.

ImMutableHasMutableField .java

package com.javacodegeeks.data;

public final class ImMutableHasMutableField {
	public static void main(String[] args) {

		MutablePOJO mutablePOJO = new MutablePOJO();
		mutablePOJO.setField("Mary");

		ImMutableHasMutableField testObj = new ImMutableHasMutableField(mutablePOJO);
		System.out.println("should print out Mary: " + testObj.toString());
		
		mutablePOJO.setField("Terry");
		System.out.println("mutablePOJO changed: " + mutablePOJO.toString());
		System.out.println("should print out Mary: " + testObj.toString());

	}

	private final MutablePOJO mutablePOJO;

	public ImMutableHasMutableField(MutablePOJO mutableField) {
		super();
		// this is how to ensure the mutablePOJO never change after the creation
		this.mutablePOJO = new MutablePOJO();
		this.mutablePOJO.setField(mutableField.getField());
	}

	public MutablePOJO getMutablePOJO() {
		// this is how to ensure the mutablePOJO never change after the creation
		// never return the mutablePOJO reference to keep
		MutablePOJO mutablePOJO = new MutablePOJO();
		mutablePOJO.setField(this.mutablePOJO.getField());

		return mutablePOJO;
	}

	@Override
	public String toString() {
		return "ImMutableHasMutableField [mutablePOJO=" + mutablePOJO.getField() + "]";
	}

}

class MutablePOJO {

	private String field;

	public String getField() {
		return field;
	}

	public void setField(String field1) {
		this.field = field1;
	}

	@Override
	public String toString() {
		return "MutablePOJO [field=" + field + "]";
	}

}

Execute it as a Java application. It should print out two lines of the same messages even the mutablePOJO changed because the class maintains its immutability. The change to the mutablePOJO did not cause side effect to the testObj.

Output

should print out Mary: ImMutableHasMutableField [mutablePOJO=Mary]
mutablePOJO changed MutablePOJO [field=Terry]
should print out Mary: ImMutableHasMutableField [mutablePOJO=Mary]

As the output displayed here, the mutablePOJO changes didn’t affect the ImMutableHasMutableField at all.

9. Deep Copy Ensures Immutability

There are three ways to copy an object:

  • Reference copy – copy the object reference from the source to destination. After that, both the source and destination have the same reference. Any change to the object will reflect both. This is not immutable at all.
  • Shallow Copy – create a new instance for the destination, but copy the reference at the children level. After that, both the source and destination’s children object share the same reference.
  • Deep Copy – Create a new instance and all the children object instance for the destination. so the Source and destination have no shared data in any sort of form. Any change made to either source or destination will not affect each other. The immutability is kept.

In this step, I create a DeepCopyDemo class which has the deepCopy() method which returns a new object. Please note that constructor of ImmutableHasMutableField creates a new instance.

DeepCopyDemo.java

package com.javacodegeeks.data;

public class DeepCopyDemo {
	public static void main(String[] args) {
		MutablePOJO mary = new MutablePOJO();
		mary.setField("Mary");

		ImMutableHasMutableField maryObj = new ImMutableHasMutableField(mary);
		DeepCopyDemo sourceObj = new DeepCopyDemo("Mary", maryObj);

		System.out.println("Source object is " + sourceObj.toString());

		DeepCopyDemo copiedObj = sourceObj.deepCopy();

		System.out.println("Copied object is " + copiedObj.toString());

		mary.setField("Zheng");
		System.out.println("mary object changed " + mary.toString());

		System.out.println("Copied object is " + copiedObj.toString());

	}

	private final String name;

	private final ImMutableHasMutableField objectField1;

	public DeepCopyDemo(String name, ImMutableHasMutableField obj) {
		super();
		this.name = name;
		this.objectField1 = obj;
	}

	public DeepCopyDemo deepCopy() {
		return new DeepCopyDemo(this.name, new ImMutableHasMutableField(this.objectField1.getMutablePOJO()));
	}

	@Override
	public String toString() {
		return "DeepCopyDemo [name=" + name + ", objectField1=" + objectField1.toString() + "]";
	}
}

Output

Source object is DeepCopyDemo [name=Mary, objectField1=ImMutableHasMutableField [mutablePOJO=Mary]]
Copied object is DeepCopyDemo [name=Mary, objectField1=ImMutableHasMutableField [mutablePOJO=Mary]]
mary object changed MutablePOJO [field=Zheng]
Copied object is DeepCopyDemo [name=Mary, objectField1=ImMutableHasMutableField [mutablePOJO=Mary]]

Output shows that a deep copied object maintains immutability from the source object.

10. Best Practices

Below we summarize the best practices for the immutable objects:

  • Initialize the immutable fields that are declared final in the constructors
  • Do not provide setters as the immutable fields cannot change
  • Use them in concurrent applications to achieve thread-safety
  • The convenient Collections methods create immutable Collections
  • The Builder pattern eases the creation of immutable objects
  • Create immutable Set elements and immutable Map keys

JDK provides several useful immutable classes. Click here to see details.

11. Java Immutable Class – Conclusion

In this post, we took a look at the immutable class in Java and how to create them using the final keyword. We saw the importance of immutability in concurrent applications. why the String class is immutable and how to make use of the Builder pattern. Finally, we took a look at the convenient Collections methods and why we should use immutability in Set elements and Map keys.

12. Download the Eclipse project

Download
You can download the full source code of the above examples here: Java Immutable Class Example

Last updated on Jun. 22nd, 2020

Lefteris Karageorgiou

Lefteris is a Lead Software Engineer at ZuluTrade and has been responsible for re-architecting the backend of the main website from a monolith to event-driven microservices using Java, Spring Boot/Cloud, RabbitMQ, Redis. He has extensive work experience for over 10 years in Software Development, working mainly in the FinTech and Sports Betting industries. Prior to joining ZuluTrade, Lefteris worked as a Senior Java Developer at Inspired Gaming Group in London, building enterprise sports betting applications for William Hills and Paddy Power. He enjoys working with large-scalable, real-time and high-volume systems deployed into AWS and wants to combine his passion for technology and traveling by attending software conferences all over the world.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Maxim Markov
Maxim Markov
4 years ago

unmodifiable collections are not the same as immutable! Unmodifiable collection is a view, so indirectly it could still be ‘modified’ from some other reference that is modifiable. More info: https://stackoverflow.com/questions/8892350/immutable-vs-unmodifiable-collection

Back to top button