Mutable objects are different control in a multi-threaded environment and are prone to cause strange bugs and unexpected behaviors in your code. We are going to investigate why it’s usually better to work with immutable objects and learn good design patterns to help you as a developer achieve immutability.
Why is mutability dangerous?
Many applications today utilize multiple threads to spread the workload across multiple processor cores. For example in an application, it is normal to keep the GUI on a separate thread. Otherwise, you would have the GUI being blocked until the back-end has finished processing stuff. But what happens when thread A has an object and then thread B updates it? Let’s construct a basic test for this.
public class MutablePerson {
private String name;
private String city;
private List favoriteDishes;
public MutablePerson(String name, String city, List favoriteDishes) {
this.name = name;
this.city = city;
this.favoriteDishes = favoriteDishes;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public List getFavoriteDishes() {
return favoriteDishes;
}
public void setFavoriteDishes(List favoriteDishes) {
this.favoriteDishes = favoriteDishes;
}
Here we got a Person class. A person has a name, a city and a list of favorite dishes. The constructor for MutablePerson will create objects are, as you probably guessed, mutable.
public static void mutableCityTest() {
MutablePerson mutablePerson = new MutablePerson("Viktor", "Stockholm", Arrays.asList("Pizza", "Tacos", "Steak"));
new Thread(() -> {
try {
Thread.sleep(5); // Simulate some work
} catch (InterruptedException e) {
e.printStackTrace();
}
mutablePerson.setCity("Gothenburg");
}).start();
if ("Stockholm".equals(mutablePerson.getCity())) {
try {
Thread.sleep(10); // Simulate some work
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Viktor lives in " + mutablePerson.getCity());
}
}
The following method creates a MutablePeson object and then a separate thread simulates some work and updates the person’s city. Simultaneously, our initial thread checks if the city is Stockholm, and if so simulate some work and then print out the city. Running our little method produces the following output:
Viktor lives in Gothenburg
This is not great, as we had a test for Stockholm we expect the person’s city to be Stockholm as well when printing it out. Let’s take a look at another example, something a bit more serious, mutable lists. Let’s introduce another person, Anastasia. She likes the same food. So she decides to use the same list of favorite foods as Viktor.
List dishes = new ArrayList<>();
dishes.add("Pizza");
dishes.add("Tacos");
dishes.add("Steak");
MutablePerson mutablePerson1 = new MutablePerson("Viktor", "Stockholm", dishes);
MutablePerson mutablePerson2 = new MutablePerson("Anastasia", "Stockholm", mutablePerson1.getFavoriteDishes());
But Anastasia also loves Sushi, so she adds that to her list.
mutablePerson2.getFavoriteDishes().add("Sushi");
Viktor then gets hungry and decides to order some food. He looks at his list of favorite foods to help him decide on what to buy.
Pizza
Tacos
Steak
Sushi
But wait, what? Viktor hates Sushi, this is a disaster!
Immutability
We saw how potentially dangerous mutability can be. So let’s attempt to create an immutable person class. I am going to introduce you to something referred to as the builder pattern. The builder patterns make it possible to force immutability by using final modifiers on all the instance variables without having a super long constructor where everything has to be set. With the final modifier and/or not providing any setter methods directly on the person class, we can be sure that our object never mutates without us knowing it.
The builder pattern often introduces a nested inner building class, where you build up the object before constructing it. When we feel ready to construct our person class we can then call the build method.
public class ImmutablePerson {
private final String name;
private final String city;
private final List favoriteDishes;
private ImmutablePerson(Builder builder) {
this.name = builder.name;
this.city = builder.city;
this.favoriteDishes = builder.favoriteDishes;
}
public String getName() {
return name;
}
public String getCity() {
return city;
}
public List getFavoriteDishes() {
return favoriteDishes != null ? new ArrayList<>(favoriteDishes) : null;
}
public Builder toBuilder() {
return new Builder(this);
}
public static Builder builder() {
return new Builder();
}
public static final class Builder {
private String name;
private String city;
private List favoriteDishes;
public Builder() {
// Empty constructor
}
public Builder(ImmutablePerson person) {
this.name = person.name;
this.city = person.city;
this.favoriteDishes = person.favoriteDishes != null ? new ArrayList<>(person.favoriteDishes) : null;
}
public Builder withName(String name) {
this.name = name;
return this;
}
public Builder withCity(String city) {
this.city = city;
return this;
}
public Builder withFavoriteDishes(List dishes) {
this.favoriteDishes = dishes != null ? new ArrayList<>(dishes) : null;
return this;
}
public ImmutablePerson build() {
return new ImmutablePerson(this);
}
}
}
We also make copies of the list and setting and retrieving the list, so that nobody outside the actual the object itself can modify their list. Let’s try the previous methods that we created with this ImmutablePerson object instead.
public static void immutableCityTest() {
ImmutablePerson immutablePerson = ImmutablePerson.builder()
.withName("Viktor")
.withCity("Stockholm")
.withFavoriteDishes(Arrays.asList("Pizza", "Tacos", "Steak"))
.build();
new Thread(() -> {
try {
Thread.sleep(5); // Simulate some work
} catch (InterruptedException e) {
e.printStackTrace();
}
immutablePerson.toBuilder()
.withCity("Gothenburg")
.build();
}).start();
if ("Stockholm".equals(immutablePerson.getCity())) {
try {
Thread.sleep(10); // Simulate some work
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Viktor lives in " + immutablePerson.getCity());
}
}
As you noticed, there isn’t even any setter to modify the instance variables, instead, we have to build a ImmutablePerson. And as you probably expected the output to be:
Viktor lives in Stockholm
Let’s try our other example as well.
public static void immutableFoodListTest() {
List dishes = new ArrayList<>();
dishes.add("Pizza");
dishes.add("Tacos");
dishes.add("Steak");
ImmutablePerson immutablePerson1 = ImmutablePerson.builder()
.withName("Viktor")
.withCity("Stockholm")
.withFavoriteDishes(dishes)
.build();
List dishes2 = immutablePerson1.getFavoriteDishes();
dishes2.add("Sushi");
ImmutablePerson immutablePerson2 = ImmutablePerson.builder()
.withName("Anastasia")
.withCity("Stockholm")
.withFavoriteDishes(dishes2)
.build();
for (String dish : immutablePerson1.getFavoriteDishes()) {
System.out.println(dish);
}
}
And the output was:
Pizza
Tacos
Steak
Viktor didn’t risk end up eating something that he didn’t enjoy! So then you may wonder, how do you edit objects? Well, you don’t. Instead, you create a new object from the existing one. I have provided an easy way to do this with toBuilder()
, which returns a builder from the object.
ImmutablePerson person = ImmutablePerson.builder()
.withName("Viktor")
.withCity("Stockholm")
.build();
ImmutablePerson updatedPerson = person.toBuilder()
.withCity("Gothenburg")
.build();
Assert.assertNotEquals(person, updatedPerson);
Assert.assertEquals("Stockholm", person.getCity());
Assert.assertEquals("Gothenburg", updatedPerson.getCity());
Final words
We have investigated the benefits of switching over and sticking to immutable objects. Of course, my examples have been a bit extreme, but it was just to prove a point. Mutable objects are a hazard in a multi-threaded application, dealing with mutable objects requires the developer to be very cautious. So why not switch over to immutable objects and get rid of that burden?
The builder pattern and immutable objects go hand in hand, so I suggest that you start using them more if you aren’t already.
The source code for this blog can be found on my GitHub here.
If you enjoyed this post, please help me out by giving it a like and sharing it on social media.
So Viktor is now bound to Stockholm for good, hope he likes it there. Also it’s nice that his favorite dishes now remain the same, even if Anastasia specifically asks to share his instance of the list. Your examples certainly seem a bit extreme.
Why is there no demonstration of how one would work with the immutable objects. Obviously there’s a requirement for being able to change the properties of a Person post-instatiation, otherwise just omit the setters from the mutable version, wrap favorite foods in a Collections.unmodifiableList() and be done with it.
Hi, it’s actually quite simple and I provided a method exactly for this but probably I didn’t make it clear enough and that’s my fault I will update the post later tonight with an example of this.
In the ImmutablePerson class there is a method where you create a new builder from the object.
With the builder object returned you can create a new object with updated values. The whole point is that you shouldn’t be allowed to modify an object. You can only create new ones.