Making JAVA objects immutable

Making JAVA objects immutable

Let us talk about the immutability of JAVA objects. This came up as a fascinating interview question in one of the companies my friend interviewed for. Since I learnt a thing or two from this, I decided to scribble about the same.

Why make objects immutable you ask?

Immutability doesn't allow any modifications to the object. This gives us a lot of advantages like:

  • Thread safety.
  • Clean and concise code.
  • Can be easily cached (for versioned data).

Also, immutability can be used to maintain versioned data. I have seen this concept used in a lot of databases where every update request to a record creates a new record with a new version number while the original record is kept intact (immutable). Thus, immutability can be easily used to maintain and serve versioned data to the client. Git is another such example (with branches implementing versioned code).

There are some potential disadvantages as well:

  • Can have a high memory footprint if not used cautiously.
  • Languages which have inbuilt garbage collection like JAVA can suffer hugely from the large time consumption in garbage collection of these objects (presumably because of huge in number) once the objects are no more used and can cause significance latency in serving requests.

Journey of making an object immutable

Let's take the simplest of examples. The famous Employee class. An Employee has a name, an id and a department. The Department is decribed using a class of its own. The HAS-A relationship between Employee and Department implies that Department is composed inside the Employee class.

We'll aim to make the Employee class immutable.

class Employee {
    public String name;
    public int employeeId;
    public Department department;

    public Employee(String name, int employeeId, Department department) {
        this.name = name;
        this.employeeId = employeeId;
        this.department = department;
    }

    public String toString() {
        return String.format("%d: %s from %s", employeeId, name, department);
    }
}

class Department {
    int departmentId;
    String name;

    public Department(int departmentId, String name) {
        this.departmentId = departmentId;
        this.name = name;
    }

    public String toString() {
        return String.format("%s", name);
    }
}

The constructor here shows the dependencies that are needed to create an Employee. Right now, I can make objects of the Employee class and can easily modify the attributes of the class. The employee object is currently mutable.

class Driver {
    public static void main(String[] args) {
        Department department = new Department(1, "Design and Development");
        Employee employee = new Employee("Rohan", 1, department);

        // Modifying the object since the object is mutable.
        employee.name = "Anonymous";
    }
}

Private keyword

The first observation is to restrict access to the data members. We can do this by using the private keyword. However, using private is not enough. Sure private won't allow us to access data members outside the class but we can still access and modify the data members inside the class. No one is stopping us from creating a function and accessing and modifying the state of the data member.

class Employee {
    private String name;
    private int employeeId;
    private Department department;

    public Employee(String name, int employeeId, Department department) {
        this.name = name;
        this.employeeId = employeeId;
        this.department = department;
    }

    // Even with `private`, we can still modify the data members of the class inside the class.
    public void modifyState() {
        this.name = "Anonymous";
    }

    public String toString() {
        return String.format("%d: %s from %s", employeeId, name, department);
    }
}

class Department {
    int departmentId;
    String name;

    public Department(int departmentId, String name) {
        this.departmentId = departmentId;
        this.name = name;
    }

    public String toString() {
        return String.format("%s", name);
    }
}

Final keyword and removal of setter functions

To prevent modification of data members inside the class, we need to take 2 measures:

  • Use the final keyword for every data member. The final keyword makes sure that the value set during initialization (in the constructor) is the ultimate value and no further modifications will be allowed.
  • Don't have any methods that may modify the state of the data member.

You may argue that removing methods from the class that modify the state of the object should suffice and that we don't need to make the data members final. However, note that no one can stop us from modifying the state of the object inside the Constructor which will always exist. To prevent this, final is necessary. See the example below:

class Employee {
    private String name;
    private int employeeId;
    private Department department;

    public Employee(String name, int employeeId, Department department) {
        this.name = name;
        this.employeeId = employeeId;
        this.department = department;

        // This is still allowed causing the object to be mutable
        this.name = "Anonymous";
    }

    // Don't have any such methods that modify the state of the class.
    /*
    public void modifyState() {
        this.name = "Anonymous";
    }
    */

    public String toString() {
        return String.format("%d: %s from %s", employeeId, name, department);
    }
}

class Department {
    int departmentId;
    String name;

    public Department(int departmentId, String name) {
        this.departmentId = departmentId;
        this.name = name;
    }

    public String toString() {
        return String.format("%s", name);
    }
}

With these changes, our Employee class shapes up as follows:

class Employee {
    private final String name;
    private final int employeeId;
    private final Department department;

    public Employee(String name, int employeeId, Department department) {
        this.name = name;
        this.employeeId = employeeId;
        this.department = department;
    }

    // Don't have any such methods that modify the state of the class.
    /*
    public void modifyState() {
        this.name = "Anonymous";
    }
    */

    public String toString() {
        return String.format("%d: %s from %s", employeeId, name, department);
    }
}

class Department {
    int departmentId;
    String name;

    public Department(int departmentId, String name) {
        this.departmentId = departmentId;
        this.name = name;
    }

    public String toString() {
        return String.format("%s", name);
    }
}

Making the class final

I have taken references from other articles like JournelDev and GeeksForGeeks which also talk about immutability of classes. These articles mention an intriguing point of making the class as final.

When I first read this, I couldn't come to terms with this particular suggestion. The point is to make the class final so as to prevent subclass creation and hence prevent adding additional data members and overriding getter methods. I am not sure why this needs to be done since the subclass is a totally different class from the parent class. It just happens to reuse a lot of parent code. If I make an object of the parent class with the above restrictions, there won't be any opportunities to modify its state even if the class is not final.

As far as I have come to realize, I guess this point revolves more around the intent of the class being complete in itself which doesn't need any extensions and/or modifications. If someone needs to extend the features of this class, then he/she should go ahead and make a new class altogether. So yeah, go ahead and make your classes final.

Is this it?

Are keywords private and final and removing setter methods all that we need to do? Sure this can't be this easy! Well it actually isn't this easy. We need to be meticulous in our knowledge of the internals of the language and that we'll make us realize that still work needs to be done.

But what vulnerabilities still remain? If you look closely, non-primitive (or user defined) dependencies can still be modified despite having the above restrictions and I'll show how.

Call by reference and user-defined dependencies of the class

At this stage, we can claim that for primitive types in a class, we have achieved immutability. However, the same can't be said for non primitive types.

Note that in JAVA, user defined dependencies are passed by reference. The Employee class depends on an instance of a Department class and an instance of the Department class will be passed by reference. What this means is that there'll only be one instance of the Department class on the heap and the address of the same will be passed to the Employee instance.

This has repercussions. In the Driver class, I have full control of department which is passed to employee. See the code snippet below.

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

        // I have full control of this object in this Driver class.
        Department department = new Department(1, "Design and Development");
        Employee employee = new Employee("Rohan", 1, department);

        // Printing the employee "before"
        System.out.println(employee); // 1: Rohan from Design and Development

        // Changing the department name
        department.name = "Quality and Assurance";

        // Printing the employee "after"
        System.out.println(employee); // 1: Rohan from Quality and Assurance

        // Employee is still mutable!!
    }
}

Thus, I can play with department howsoever I want. I can change the department name and since it is passed by reference, the underlying employee will also get changed since one of its dependencies have changed. You can verify this by printing employee before and after the change in department. There's only instance of department on the heap and both the Driver and the Employee class are referring to the same instance.

Note: One may claim that the final keyword with the department object should prevent the object from any modifications. However, also note that in this scenario, the variable department will hold the address of the object on the heap which won't change since we are not moving it in memory. Thus, making changes to the data of the department object in memory won't change its address and hence the final keyword won't complain.

Deep copying

The most obvious solution to this problem would be to make copies of every non primitive dependency passed inside the constructor like below simulating a call by value even for user defined types.

I have added a static function deepcopy inside the Department class which will make a deep copy of a department instance passed in the function.

class Employee {
    private final String name;
    private final int employeeId;
    private final Department department;

    public Employee(String name, int employeeId, Department department) {
        this.name = name;
        this.employeeId = employeeId;
        this.department = Department.deepcopy(department);
    }

    // Don't have any such methods that modify the state of the class.
    /*
    public void modifyState() {
        this.name = "Anonymous";
    }
    */

    public String toString() {
        return String.format("%d: %s from %s", employeeId, name, department);
    }
}

class Department {
    int departmentId;
    String name;

    public Department(int departmentId, String name) {
        this.departmentId = departmentId;
        this.name = name;
    }

    public static Department deepcopy(Department d) {
        Department copiedDepartment = new Department(d.departmentId, d.name);
        return copiedDepartment;
    }

    public String toString() {
        return String.format("%s", name);
    }
}

This solution will work absolutely fine. We have achieved immutability of the Employee class. However, with respect to the maintainability of the code, there's a slight improvement that we can perform.

Cloneable interface

Writing the deepcopy() function in every dependent class will eventually become tedious. As the codebase grows, the Employee class may start depending on more classes and we'll end up adding a deepcopy function in each of those dependent classes. We can improve the code maintainability by leveraging a feature from the JAVA language.

In JAVA, every class is a subclass of the Object class. The Object class has a method called clone which is responsible for making a deepcopy of an object. Therefore, every user defined class also has a clone method. Hence, we can make use of this method to create deep copies of the dependent classes.

However, if we directly try to invoke the clone method, we'll get a CloneNotSupportedException. This is because every class that needs to use the clone method needs to implement the marker interface Cloneable. See the snippet below where the Department class implements the Cloneable interface by implementing the clone method.

class Employee {
    private final String name;
    private final int employeeId;
    private final Department department;

    public Employee(String name, int employeeId, Department department) {
        this.name = name;
        this.employeeId = employeeId;
        this.department = (Department) department.clone();
    }

    // Don't have any such methods that modify the state of the class.
    /*
    public void modifyState() {
        this.name = "Anonymous";
    }
    */

    public String toString() {
        return String.format("%d: %s from %s", employeeId, name, department);
    }
}

class Department implements Cloneable {
    int departmentId;
    String name;

    public Department(int departmentId, String name) {
        this.departmentId = departmentId;
        this.name = name;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    public String toString() {
        return String.format("%s", name);
    }
}

After implementing the clone method, we can easily make any class immutable.

What about the String class?

A valid question that may come to the inquisitve mind is regarding the deep copy of strings. Note that I did claim that immutability will be achieved once we made deep copies of all the dependent objects. I didn't make any comment on the strings.

In the Employee class, we also had a string dependency named name. We don't need to do any kind of cloning for the string dependency. This is because strings in JAVA are immutable. Even if I try to change the string from the Driver class, a new string will be created and the original string will be intact. So yeah, no worries!

Should you choose JAVA lest you encounter immutability?

Well you should definitely look at other alternatives before going forward with JAVA.

Why? There are many reasons not to choose JAVA and even more reasons to choose other languages if you have to play around with immutability. Functional programming languages like Haskell do not allow any mutations and hence maybe a good choice for this purpose.

Some reasons not to choose JAVA:

  • Java is just simply not designed to handle immutability. JVM doesn't enforce instantial immutability in its type system. Java only supports binding immutability. Consider someFunction(final Employee employee). Despite employee being declared final, the only restriction is that employee = someOtherEmployee(); is illegal. This is an example of binding immutability. In C++, const Employee& employee implies that employee = someOtherEmployee(); is illegal but employee.state = someOtherState is also illegal. C++ provides both binding immutability as well as instantial immutability with const T&.
  • Java doesn't provide any reference checkers. For example, the JVM has no problem producing two mutable references in two different threads at the same time; this can cause disasters if everything isn't explicitly made thread-safe, and in Java, things are generally difficult to make that thread-safe manually.

Java (from versions 8 and onwards) have this notion of a record class that is by design immutable.

JAVA doesn't have the cleanest of solutions for sure but many systems are still using JAVA and they can't migrate their codebase to some other language overnight so they might need to make their classes immutable to cater to business logic.

Conclusion

So this is it. Hope you learned a thing or two! If you found this article relevant, do share it with your professional network.

About me

My name is Rohan and I am pursuing my MS in CS from Stony Brook University. I am highly passionate about tech and engineering. Working on projects related to databases, distributed systems and software defined networking. You can follow me on GitHub and LinkedIn

Errata

If you found any issues with this article, please email to .