Protect Your Immutable Object Invariants in More Complex Java Objects
Learn the basics on how to write robust immutable Java objects and reap the benefits of immutable objects in your code.
Join the DZone community and get the full member experience.
Join For FreeWhat are Immutable Objects?
By using Immutable Objects, which are objects that can not be observed (from the outside) to change once they are created, we gain a number of advantages including inherent thread safety, improved run time efficiency and code robustness. But how do we protect our object's invariants (i.e. how do we protect the internal state of the objects, so that it will fullill the requirements on them, in this case that they should never change)? This post shows some of the fundamental schemes that can be used to ensure that our objects remain immutable.
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.
If we run the following test program, we expose a problem with the "immutable" object that really makes it mutable.
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 cannot 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. We can use them in our 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...
Remember "To Protect and to Serve"
Published at DZone with permission of Per-Åke Minborg, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments