Mutability #
Illustration #
The Java method createUsers
below is incorrect.
Can you see why, and how to fix this?
public class User {
int id;
String name;
}
/*
Input:
- ids: a nonempty array of user identifiers
- names: an array of the same length as 'ids' that contains
user names
Ouput: an array of users of the same lenght as 'ids',
such as the i-th user has identifier ids[i] and name names[i]
*/
User[] createUsers(int[] ids, String[] names){
User[] users = new User[names.length];
User currentUser = new User();
for(int i = 0; i < ids.length; i++){
currentUser.id = ids[i];
currentUser.name = names[i];
users[i] = currentUser;
}
return users;
}
The output array contains $i$ times the same user (with the last id and name from the input arrays).
One way to fix this is:
- add an explicit constructor to the class
User
:
public class User {
int id;
String name;
public User(int id, String name){
this.id = id;
this.name = name;
}
}
- call this constructor for each new user:
public User[] createUsers(int[] ids, String[] names) {
User[] users = new User[names.length];
for(int i = 0; i < ids.length; i++){
users[i] = new User(ids[i], names[i]);
}
return users;
}
In this example, an instance of the class User
is a mutable object, meaning that its attributes (id
and name
) can be modified after the object is created.
However, these two values (or at least the value of the attribute id
) are unlikely to change.
In Java (or C#, C++, etc.), it is possible to forbid these two values to be modified. This would have prevented compilation of the incorrect program above.
Immutable object #
Informally, an object is mutable if it can be modified after its creation.
Immutability has many known benefits (but also drawbacks):
Benefits #
- Easier debugging: as illustrated above, forcing an object to be immutable may prevent an incorrect program to compile. Compile-time errors are usually easier to fix that bugs.
- Readability: code that modifies or reuses objects can be harder to understand (and reason about) than code that creates (fresh) immutable objects.
- Thread-safety: multiple threads can access an immutable object concurrently without race condition.
- Easier collaboration and maintenance: Alice can safely pass an immutable object to Bob (i.e. make the object visible to Bob’s code). This will not affect the correctness of Alice’s code (because Bob’s code cannot modify this object).
Reference. For a more in-depth analysis of the benefits of immutability, we refer to Effective Java, item 17:
“Classes should be immutable unless there’s a very good reason to make them mutable.”
Observation. Some programming language (like Haskell or Rust), enforce (a form of) immutability by default.
Drawbacks #
- Performance: on a large scale (e.g. thousands of objects), reusing existing object may be more efficient that creating (fresh) immutable ones.
Terminology #
The term “immutable object” (or “immutable class”) is used with slightly different meanings. In particular:
- a weaker notion of immutability only requires the attributes of an object to be non-modifiable,
- a stronger notion also requires the objects that are referred to (transitively) to be non-modifiable.
By convention, we will use in what follows the term “final” for the weaker requirement, and “immutable” for the stronger requirement. More precisely:
Definition. An object is final if its attributes cannot be modified after the object’s creation.
Definition. An object is immutable if it is final and the other objects that it references are immutable.
in Java #
final
#
The Java keyword final
ensures that a variable cannot change value after its initialization.
Example. The following Java program does not compile.
final int a = 2; a = 3;
Warning. This meaning of the keyword
final
is different from that we already saw in the section on encapsulation.
When the variable is an instance attribute, this also forces the attributes to be explicitly instantiated before the execution of the constructor terminates.
Example. The following Java program does not compile.
public class User { final int id; final String name; public User(int id){ this.id = id; } }
Example. The following Java program compiles, and the instances of
User
are immutable (because strings in Java are themselves immutable).public class User { final int id; final String name; public User(int id, String name){ this.id = id; this.name = name; } }
Note. Alternatively, a
final
instance attribute can be instantiated immediately after it is declared (because this instruction is executed before the constructor). For instance, the following program compiles.public class User { public final int id; public final String name = "Alice"; public User(int id){ this.id = id; } }
In practice. Your IDE is likely to create
final
(andprivate
) instance attributes by default.
final
vs immutable
#
Warning. The
final
keyword may not be sufficient to enforce immutability (at least as it is defined above), in particular when afinal
attribute refers to a mutable object.
Example. Instances of the class
User
below are final, but not immutable, because instances ofAddress
are not immutable.public class User { final int id; final Address address; public User(int id, Address address){ this.id = id; this.address = address; } } public class Address { int streetNumber; String street; int zipCode; public Address(int streetNumber, String street, int zipCode){ this.streetNumber = streetNumber; this.street = street; this.zipCode = zipCode; } }
For instance, the following code compiles.
Address myAddress = new Address(14, "via Goethe", 39100); User myUser = new User(1, myAddress); myAddress.streetNumber = 12;
Final and immutable types #
We already encountered several types of Java object and quasi-objects that are:
- final:
- records,
- sets (resp. lists, maps) that are declared with
Set.of()
(resp.List.of()
,Map.of()
),
- immutable:
- strings,
- boxed types (like
Integer
orBoolean
).
Record #
A record is final.
Examples. Because strings are immutable, the following records are immutable:
record User(int id, String name){};
However, the following records are only final (unless instance of
Address
are immutable):record User(int id, String name, Address address){};
Set, List, Map #
If a set (resp. list, map) is created with Set.of()
(resp. List.of()
, Map.of()
), then its content is final.
Example. The following Java program throws an
UnsupportedOperation
exception.Set<Integer> mySet = Set.of(2, 3); mySet.add(5);
However, if the set (resp. list, map) contains references, then the objects that are referred to may be mutable.
Example. The following program compiles (assuming that the attribute
streetNumber
of the classAddress
does not have thefinal
keyword).Address myAddress = new Address(14, "via Goethe", 39100); Set<Address> mySet = Set.of(myAddress); myAddress.streetNumber = 12;
By design, most other implementations of List
, Set
and Map
are not final (this is expected, for performance reasons).
However, in some scenarios, a program may create a (small) list (resp. set, map) that is not meant to be modified. For instance, the list of all files in a folder. In this case, the list (resp. set, map) can be made final. A common way to achieve this is the class ImmutableList (resp. ImmutableSet, ImmutableMap) of the Guava library.
To use Guava in a Maven project, declare this dependency:
<dependencies>
...
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>33.1.0-jre</version>
</dependency>
...
</dependencies>
or in a Gradle project:
implementation group: 'com.google.guava', name: 'guava', version: '33.1.0-jre'
String #
Java’s strings are immutable.
However, the class StringBuilder implements a mutable string.
It provides instance methods like append
, insert
, delete
(a substring), etc.
Boxed types #
Java’s boxed types (like Integer
or Boolean
) are immutable.
However, each boxed types has a mutable “atomic” counterpart, e.g. AtomicInteger for Integer
and AtomicBoolean for Boolean
.
The main purpose of these classes is to offer build-in thread safety for common sequences of operations.
For instance, AtomicInteger
provides a method equivalent to i++
, but thread-safe (note that i++
is a shortcut for three operations: read the value, increment it and write it back).
What does the following program output?
Boolean b = true;
// Create an atomic boolean with value true
AtomicBoolean ab = new AtomicBoolean(true);
List<Boolean> booleans = new LinkedList<>();
List<AtomicBoolean> aBooleans = new LinkedList<>();
booleans.add(b);
aBooleans.add(ab);
b = false;
// Set the value of the atomic boolean to false
ab.set(false);
booleans.add(b);
aBooleans.add(ab);
System.out.println(booleans);
System.out.println(aBooleans);
[true, false]
[false, false]