73 Why Is Final Added but Still Cannot Guarantee Immutability

73 Why is final Added But Still Cannot Guarantee Immutability #

In this lesson, we mainly explain why “immutability” cannot be achieved even with the addition of final.

What is immutability? #

To answer the above question, we first need to know what immutability is. If an object cannot be modified after it is created, then it has “immutability”.

Let’s take an example, the Person class below:

public class Person {

    final int id = 1;

    final int age = 18;

}

If we create a person object, it will have two attributes, namely id and age. And because they are both final, once the person object is created, the values of its attributes, id and age, cannot be changed. If we try to change the value of any attribute, we will get a compilation error. For example, the code below will not compile:

public class Person {

    final int id = 1;

    final int age = 18;

    public static void main(String[] args) {

        Person person = new Person();

//        person.age=5;//Compilation error, cannot modify the value of a final variable

    }

}

If we try to change the age of this person object to 5, it will result in a compilation error. Therefore, an object like this person object has immutability, which means its state cannot be changed.

When final modifies an object, only the reference is immutable #

There is an important point here, that is, when we use final to modify a variable that points to an object type (not to one of the 8 primitive types, such as int), the final keyword only ensures that the reference of the variable is immutable, while the content of the object itself can still be changed. Let’s explain this in detail.

In the previous lesson, we mentioned that a final variable, once assigned a value, cannot be modified, which means it can only be assigned once. If we try to reassign a variable that has already been marked as final, it will result in a compilation error. Let’s use the code below to illustrate:

/**
 
 * Description: A final variable once assigned cannot be modified
 
 */
 
public class FinalVarCantChange {

    private final int finalVar = 0;

    private final Random random = new Random();

    private final int array[] = {1,2,3};

    public static void main(String[] args) {

        FinalVarCantChange finalVarCantChange = new FinalVarCantChange();

//        finalVarCantChange.finalVar=9;     //Compilation error, not allowed to modify final variable (primitive type)

//        finalVarCantChange.random=null;    //Compilation error, not allowed to modify final variable (object)

//        finalVarCantChange.array = new int[5];//Compilation error, not allowed to modify final variable (array)

    }

}

Here, we first create an int variable, a Random variable, and an array, all of which are marked as final. Then we try to modify them, such as changing the value of the int variable to 9, setting the random variable to null, or assigning a new content to the array. None of this code will compile.

This proves that “a final variable, once assigned a value, cannot be modified”. For primitive types, this rule is unambiguous. However, for object types, final only ensures that the reference of the variable is immutable, while the object itself can still be changed. This also applies to arrays because arrays are also objects in Java. Let’s take an example to see the output of the following Java program:

class Test {

    public static void main(String args[]) {

       final int arr[] = {1, 2, 3, 4, 5};  //  Note that the array arr is final

       for (int i = 0; i < arr.length; i++) {

           arr[i] = arr[i]*10;

           System.out.println(arr[i]);

       }

    }

}

First, let’s guess, without looking at the output below, what do you think this code will print? In this code, there is a Test class with only one main method, and inside the method, there is a final array arr. Note that the array is marked as final, so it means once assigned, the reference of the array cannot be modified. But what we want to prove now is that the content of the array object can still be modified. Next, we use a for loop to multiply each element in the array by 10, and finally print the result. The output will be as follows:

10 

20 

30 

40 

50

As we can see, the printed output is 10 20 30 40 50, not the original 1 2 3 4 5, which proves that although the array arr is declared as final, its content can still be modified.

Similarly, the same is true for non-array objects. Let’s look at the following example:

class Test {
    int p = 20;
    
    public static void main(String args[]){
        final Test t = new Test();
        
        t.p = 30;
        
        System.out.println(t.p);
    }
}

In this Test class, there is an int property p. After creating a new instance t in the main function and declaring it as final, we try to change the value of its member variable p and print the result. The program will print “30”. Initially, the value of p is 20, but after modification, it becomes 30, indicating that the modification was successful.

From this, we can conclude that when final is used to modify a variable pointing to an object, the content of the object can still be changed.

Relationship between final and immutability #

This raises the question of what the relationship is between final and immutability.

Let’s compare final and immutability more specifically. The final keyword ensures that the reference to a variable remains unchanged, but immutability means that once an object is created, its state cannot be changed. It emphasizes the content of the object itself, not the reference. Therefore, final and immutability are different.

For an object of a class, you must ensure that after it is created, all internal states (including the internal properties of its member variables) never change in order to possess immutability. This requires that the state of all member variables must not be allowed to change.

There is a saying that “the simplest way to ensure immutability of an object is to declare all properties of the class as final”. This rule is not entirely correct and usually only applies when all attributes of a class are of primitive types. For example, in the previous example:

public class Person {
    final int id = 1;
    final int age = 18;
}

The Person class has two properties, final int id and final int age, both of which are of primitive types and also declared as final, so objects of the Person class do have immutability.

However, if a class has a member variable that is final and is not of a primitive type but an object type, then the situation is different. With the foundation we have learned earlier, we know that if the content of an object type attribute is modified and it is declared as final, it means that the entire class does not possess immutability. Thus, if the content of an object type changes, it means that the entire class does not possess immutability.

Therefore, we can conclude: immutability does not mean that simply using final to modify all properties of a class will make objects of that class immutable.

This raises a big question: assuming my class has a member variable of an object type, how can I ensure that the entire object is immutable?

Let’s take an example of an object of a class with a member variable of an object type that is immutable.

Here is the code:

public class ImmutableDemo {
    private final Set<String> lessons = new HashSet<>();
    
    public ImmutableDemo() {
        lessons.add("Lesson 01: Why is there only one way to implement threads?");
        lessons.add("Lesson 02: How to stop threads correctly? Why is the method with the volatile flag incorrect?");
        lessons.add("Lesson 03: How do threads transition between six states?");
    }
    
    public boolean isLesson(String name) {
        return lessons.contains(name);
    }
}

In this class, there is a final and private Set object lessons, which is a HashSet. In the constructor, we add three values to this HashSet, which are the titles of the first three lessons. The class also has a method, isLesson, which checks if the input parameter belongs to the titles of the first three lessons using the contains method of the lessons set. For an object of the ImmutableDemo class, it is immutable. Because the lessons object is final and private, the reference stays the same and cannot be accessed from outside the class. The ImmutableDemo class also does not have any methods to modify the content contained in lessons. Only the initial values are added in the constructor. Therefore, once the ImmutableDemo object is created, that is, once the constructor is executed, there is no chance to modify the data in lessons anymore. For the ImmutableDemo class, it only has this one member variable, and once this member variable is initialized, it cannot be changed. This makes the ImmutableDemo object immutable, which is a good example of an “object of a class with a member variable of an object type that is immutable”.

Summary #

In this lesson, we first introduced what immutability is, and then explained that when final is used to modify a variable of an object type, it can only guarantee that its reference remains unchanged, but the content of the object itself can still change.

Then, we discussed the relationship between the final keyword and immutability. We know that simply declaring all member variables as final does not mean that objects of the class are immutable.