Implement Immutable Classes with Java

Salitha Chathuranga
7 min readJun 22, 2022

--

Let’s create classes and play with immutability

You know, sometimes we need to create Immutable classes for some purposes such as not allowing outside world to modify the objects. We have several inbuilt classes in Java also. Some of them are:

String, Wrapper Classes, Arrays, LocalDate, LocalTime and etc...

What is Immutability?

Immutability is the ability keep not changing with the modifications. Once it s defined no one can alter it

Immutable class is a class which is once created, it’s contents can not be changed.

You may remember String class! Right? It’s the most popular Immutable class in Java.

So, how do we define a completely immutable custom class using Java? That’s the discussion I’ going to do…There are main steps to follow. I wil first list them and later go into details.

  1. Declare class as final.
  2. Make all properties as private final.
  3. Do not declare setters. Only getters.
  4. Declare all args constructor.
  5. If there are custom nested objects in the class as properties,, implement clone.
  6. If there are other types of nested objects as properties, perform a deep copy.

Let’s take an example and see…

Imagine we have a Employee class with the following properties.

String empName;
int age;
Address address;
List<String> phoneNumbers;
Map<String, String> metadata;

Step 1: Make all final

We have to go for a final class because any class cannot override this class later. And all the properties must be private — then outside the class, they are not visible: encapsulated. When we make all properties final, they must be assigned with values within the constructor! Following these we will have the class like this.

Employee:

final class Employee {
private final String empName;
private final int age;
private final Address address;
private final List<String> phoneNumbers;
private final Map<String, String> metadata;

public Employee(String name, int age, Address address, List<String> phoneNumbers, Map<String, String> metadata) {
super();
this.empName = name;
this.age = age;
this.address = address;
this.phoneNumbers = phoneNumbers;
this.metadata = metadata;
}

public String getEmpName() {
return empName;
}

public int getAge() {
return age;
}

public Address getAddress() {
return address;
}

public List<String> getPhoneNumbers() {
return phoneNumbers;
}

public Map<String, String> getMetadata() {
return metadata;
}
}

Address:

final class Address {

private String street;
private String city;

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

public String getStreet() {
return street;
}

public String getCity() {
return city;
}

@Override
public String toString() {
return "{Street: " + street + ", City: " + city + "}";
}

}

All good..Do you think so? Think what happens to phone numbers list and metadata map after we declare the object!! Anyone can add elements to both of them..But how?

Address address = new Address("street 1", "city X");
List<String> phoneNumbers = new ArrayList<>();
phoneNumbers.add("123456");
phoneNumbers.add("456789");
Map<String, String> metadata = new HashMap<>();
metadata.put("hobby", "Watching Movies");
// Declare the employee
Employee e = new Employee("John", 23, address, phoneNumbers, metadata);
// Update details
e.getPhoneNumbers().add("345123");
e.getMetadata().put("skill", "Java");
e.getMetadata().put("designation", "HR");

System.out.println(e.getPhoneNumbers());
System.out.println(e.getMetadata());

Result: Newly updated data have been highlighted here.

[123456, 456789, 345123]
{skill=Java, designation=HR, hobby=Watching Movies}

See the phone numbers and metadata have been update! This breaks immutability! 😕

We can even delete records from our “Immutable class”… You will loose data after this…

e.getMetadata().remove("hobby");
e.getPhoneNumbers().remove("123456");

This breaks immutability again! 😕 So, how we should manage this? See the next step..

Step 2: Copy the objects and return

Next thing we have to do when we have nested Objects as properties is: take a copy of the existing data assigned using constructor and return as a new object. Then whenever you call GETTERS, you will get the old data. Not the updated ones. Let’s do this to getters of phoneNumbers and metadata.

final class Employee {
private final String empName;
private final int age;
private final Address address;
private final List<String> phoneNumbers;
private final Map<String, String> metadata;

public Employee(String name, int age, Address address, List<String> phoneNumbers, Map<String, String> metadata) {
super();
this.empName = name;
this.age = age;
this.address = address;
this.phoneNumbers = phoneNumbers;
this.metadata = metadata;
}

public String getEmpName() {
return empName;
}

public int getAge() {
return age;
}

public Address getAddress() {
return address;
}

// copy the list of phone numbers
public List<String> getPhoneNumbers() {
return new ArrayList<>(phoneNumbers);
}

// copy the map of metadata
public Map<String, String> getMetadata() {
return new HashMap<>(metadata);
}

}

See..Even if we now add elements from client code it won’t e stored inside employee object! It will always return the initialized objects…Not we cannot add data into phoneNumbers and metadata. Cool 😎

Anything remaining❓

What about Address custom object? Let’s update it with some setters and see..

final class Address {

private String street;
private String city;

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

public String getStreet() {
return street;
}

public void setStreet(String street) {
this.street = street;
}


public String getCity() {
return city;
}

public void setCity(String city) {
this.city = city;
}


@Override
public String toString() {
return "{Street: " + street + ", City: " + city + "}";
}

}

Can we update city and street in the address after declaring it in the client code?? YES!!! we can! See this.

e.getAddress().setCity("c3");
e.getAddress().setStreet("s3");

See the result..You will have the updated address inside Employee! It will be like this now => {Street: s3, City: c3}

This breaks immutability again! Still our class is partially mutable 😕 So, how we should manage this? See the next step..

Step 3: Clone custom objects

In a situation like this, we have to implement clone inside custom object: Address. Then Java will make sure that it is always giving a clone of the already declared object of address.

final class Address implements Cloneable {

private String street;
private String city;

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

public String getStreet() {
return street;
}

public void setStreet(String street) {
this.street = street;
}

public String getCity() {
return city;
}

public void setCity(String city) {
this.city = city;
}

public Object clone() throws CloneNotSupportedException {
return super.clone();
}


@Override
public String toString() {
return "{Street: " + street + ", City: " + city + "}";
}

}

And then when we get Address in Employee class, we should return a clone!

final class Employee {
private final String empName;
private final int age;
private final Address address;
private final List<String> phoneNumbers;
private final Map<String, String> metadata;

public Employee(String name, int age, Address address, List<String> phoneNumbers, Map<String, String> metadata) {
super();
this.empName = name;
this.age = age;
this.address = address;
this.phoneNumbers = phoneNumbers;
this.metadata = metadata;
}

public String getEmpName() {
return empName;
}

public int getAge() {
return age;
}

// clone the address object
public Address getAddress() throws CloneNotSupportedException {
return (Address) address.clone();
}


// copy the list of phone numbers
public List<String> getPhoneNumbers() {
return new ArrayList<>(phoneNumbers);
}

// copy the map of metadata
public Map<String, String> getMetadata() {
return new HashMap<>(metadata);
}
}

Now try with the below client code and see the results...

Address address = new Address("street 1", "city X");
List<String> phoneNumbers = new ArrayList<>();
phoneNumbers.add("123456");
phoneNumbers.add("456789");
Map<String, String> metadata = new HashMap<>();
metadata.put("hobby", "Watching Movies");

// Declare the employee
Employee e = new Employee("John", 23, address, phoneNumbers, metadata);

// Update details
e.getPhoneNumbers().add("345123");
e.getMetadata().put("skill", "Java");
e.getMetadata().put("designation", "HR");

// change address details
e.getAddress().setCity("c3");
e.getAddress().setStreet("s3");

System.out.println(e.getPhoneNumbers());
System.out.println(e.getMetadata());
System.out.println(e.getAddress());

Is the Employee object changed? NO..Right? Now it’s keeping constant against whatever the change we do to change its data!

Now we have achieved IMMUTABILITY 😍 💪 😊

Final class setup will be like this with client code.

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class ImmutableClassDemo {

public static void main(String[] args) throws CloneNotSupportedException {

Address address1 = new Address("s1", "c1");
List<String> phoneNumbers = new ArrayList<>();
phoneNumbers.add("123345");
phoneNumbers.add("456789");
Map<String, String> metadata = new HashMap<>();
metadata.put("hobby", "Watching Movies");
Employee e = new Employee("John", 23, address1, phoneNumbers, metadata);

// modifications
e.getAddress().setCity("c3");
e.getAddress().setStreet("s3");
e.getPhoneNumbers().add("1234");
e.getMetadata().put("skill", "Java");
e.getMetadata().put("designation", "HR");

System.out.println(e.getEmpName());
System.out.println(e.getAge());
System.out.println(e.getAddress());
System.out.println(e.getPhoneNumbers());
System.out.println(e.getMetadata());

}
}

final class Employee {
private final String empName;
private final int age;
private final Address address;
private final List<String> phoneNumbers;
private final Map<String, String> metadata;

public Employee(String name, int age, Address address, List<String> phoneNumbers, Map<String, String> metadata) {
super();
this.empName = name;
this.age = age;
this.address = address;
this.phoneNumbers = phoneNumbers;
this.metadata = metadata;
}

public String getEmpName() {
return empName;
}

public int getAge() {
return age;
}

// clone the address object
public Address getAddress() throws CloneNotSupportedException {
return (Address) address.clone();
}

// deep copy the list of phone numbers
public List<String> getPhoneNumbers() {
return new ArrayList<>(phoneNumbers);
}

// deep copy the map of metadata
public Map<String, String> getMetadata() {
return new HashMap<>(metadata);
}
}

final class Address implements Cloneable {

private String street;
private String city;

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

public String getStreet() {
return street;
}

public void setStreet(String street) {
this.street = street;
}

public String getCity() {
return city;
}

public void setCity(String city) {
this.city = city;
}

public Object clone() throws CloneNotSupportedException {
return super.clone();
}

@Override
public String toString() {
return "{Street: " + street + ", City: " + city + "}";
}

}

This is all about implementing a custom immutable class! Try this and use in your day to day Java coding!

Bye! Happy Coding! ❤️

--

--

Salitha Chathuranga
Salitha Chathuranga

Written by Salitha Chathuranga

Associate Technical Lead at Sysco LABS | Senior Java Developer | Blogger

Responses (5)