Background
By using
Immutable Objects, which are objects that can not be observed to change once they are created, you gain a number of advantages including inherent thread safety, improved run time efficiency and code robustness. But how do you protect your object's invariants? This post shows some of the fundamental schemes that can be used to ensure that your objects remain immutable. The examples in this post requires Java 8 but it is relatively easy to rewrite them in Java 7 or lower.
Immutable objects are used extensively in the open-source project Speedment that I am contributing to. With Speedment we can view database tables as standard Java 8 streams. Check out
Speedment on GitHub.
Simple Immutable Objects
In my
Previous post, I talked a lot about how one can create immutable objects using the
Builder Pattern. In this post I will use a more simple approach to create the objects, because I want to focus on another aspect of the immutable objects.
Consider the following Object:
public class Author {
private final String name;
private final int bornYear;
public Author(final String name, final int bornYear) {
this.name = name;
this.bornYear = bornYear;
}
public String getName() {
return name;
}
public int getBornYear() {
return bornYear;
}
}
The object's invariants are protected by the
final declarations (that make it impossible to write code
in the class that directly changes the properties of the Object), by the
private declarations (that make sure that
no other class can gain access to the fields) and by the fact that there are no setters for the object's properties (it would in fact, not be possible to write setters because the fields are declared
final). Now is a good time to mention that your objects can, in theory, be modified anyhow, for example using
Java Reflection. However, this is considered "cheating" ...
Now if we run the following test program we get exactly what one would expect.
public class Main {
public static void main(String[] args) {
final Author author = new Author("William Shakespeare", 1564);
System.out.println(author.getName() + " was born in " + author.getBornYear());
}
}
William Shakespeare was born in 1564
N.B. Even though
author is declared final, this does not affect how methods can be called on the object itself. It only says that the object reference variable
author can be assigned only once.
Complex Immutable Objects
Some objects contain more complex properties such a Maps, Sets, Lists, Collections and the likes. Consider the following
Author object with an added property consisting of a
List of the author's works.
import java.util.List;
public class Author {
private final String name;
private final int bornYear;
private final List<String> works;
public Author(final String name, final int bornYear, final List<String> works) {
this.name = name;
this.bornYear = bornYear;
this.works = works;
}
public String getName() {
return name;
}
public int getBornYear() {
return bornYear;
}
public List<String> getWorks() {
return works;
}
}
If we run the following test program, we expose a problem with the "immutable" object that really makes it mutable.
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
final List<String> works = Stream.of("Hamlet", "Othello", "Macbeth")
.collect(Collectors.toList());
final Author author = new Author("William Shakespeare", 1564, works);
println(author);
// NOT GOOD! We can add things to the list after the object is created!
author.getWorks().add("Harry Potter");
println(author);
}
private static void println(final Author author) {
System.out.println(author.getName() + " was born in " + author.getBornYear()
+ " and wrote " + author.getWorks().stream().collect(Collectors.joining(", ")));
}
}
William Shakespeare was born in 1564 and wrote Hamlet, Othello, Macbeth
William Shakespeare was born in 1564 and wrote Hamlet, Othello, Macbeth, Harry Potter
The
getWorks() method returns a reference to the same
List that we used to construct the
Author. Because the original list used during construction was writable, we can now change this
List, for example we can add "Harry Potter" to William Shakespeare's list of works! Not good! Back to the drawing board!
UnmodifiableList
By using a static method from the
Collections class, we can create a view of an existing
List that prevents the underlying
List from being modified. This is nice and thus we make a new attempt to fix the problem:
import java.util.Collections;
import java.util.List;
public class Author {
private final String name;
private final int bornYear;
private final List<String> works;
public Author(final String name, final int bornYear, final List<String> works) {
this.name = name;
this.bornYear = bornYear;
this.works = Collections.unmodifiableList(works);
}
public String getName() {
return name;
}
public int getBornYear() {
return bornYear;
}
public List<String> getWorks() {
return works;
}
}
And here is the corresponding test program:
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
final List<String> works = Stream.of("Hamlet", "Othello", "Macbeth").collect(Collectors.toList());
final Author author = new Author("William Shakespeare", 1564, works);
println(author);
// We failed again because we can modify the works List
// and it reflects in the Author after creation
works.add("Harry Potter");
println(author);
// This works though!
author.getWorks().add("Harry Potter 2");
}
private static void println(final Author author) {
System.out.println(author.getName() + " was born in " + author.getBornYear()
+ " and wrote " + author.getWorks().stream().collect(Collectors.joining(", ")));
}
}
William Shakespeare was born in 1564 and wrote Hamlet, Othello, Macbeth
William Shakespeare was born in 1564 and wrote Hamlet, Othello, Macbeth, Harry Potter
Exception in thread "main" java.lang.UnsupportedOperationException
at java.util.Collections$UnmodifiableCollection.add(Collections.java:1115)
at com.blogspot.minborgsjavapot.immutables._4unmod2.Main.main(Main.java:20)
Again we fail, because even though we now cannot change the
List by the reference returned by the
getWorks() method, we can still use the original
List, used during construction of the
Author object, to change the works list after the immutable is created. This is a clear violation against the definition of an immutable object (remeber, no observable change shall be detected after an immutable object is created).
Defensive Copying
By employing
Defensive Copying we can protect the immutable object's more complex invariants as shown in the example below:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class Author {
private final String name;
private final int bornYear;
private final List<String> works;
public Author(final String name, final int bornYear, final List<String> works) {
this.name = name;
this.bornYear = bornYear;
// Now we make a new List that is a copy of the provided works list
this.works = Collections.unmodifiableList(new ArrayList<>(works));
}
public String getName() {
return name;
}
public int getBornYear() {
return bornYear;
}
public List<String> getWorks() {
return works;
}
}
Finally, we can not change the
List of works after object creation. The price is that we need to make a new internal copy of the list that is provided during object creation. This can sometimes be a good thing, since we can select the implementation of the internal
List in a way that it can be more efficient than the original
List. For example, if the
List only consists of one element, one can create a defensive copy using the
Collections.singletonList() that creates a specialized implementation of a
List with exactly one element, potentially much more efficient than a general
List. If the List is empty, one can use the
Collections.emptyList() that is even more efficient.
The Collections class
There are several useful methods in the Collections class with respect to protecting immutable objects including unmodifiableCollection(), unmodifiableList(), unmodifiableMap() and unmodifiableSet() and more. Use them in your immutable classes!
A Final Warning
In the examples above, we had a list of Strings and we draw to our mind that the class
String, for good reasons, is immutable. But suppose that we had a
List of some mutable objects such as StringBuilders or other Lists. Then we would have to make defensive copies of them too, recursively until we finally arrive at an immutable object...
Tip: If you only work with immutable objects within your immutable objects then you are better off...
Try it!
Take the code for a spin using "git clone https://github.com/minborg/javapot.git"
Remember "To Protect and to Serve"