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.