Skip to content
Why objects must be Immutable

Why objects must be Immutable

Immutable objects have multiple advantages over mutable objects which makes them the perfect building block to create software. You may already have many questions about the subject like:

  • Why should I use immutable over mutable objects?
  • How can I design software where no single object can be modified?

You'll see in this article what an immutable object is, the advantages to use them every time you can and how they can speed up your software developments.

Definition

To make it simple, a class is immutable when instances of this class cannot be changed once constructed. Immutable objects follow five rules, like explained in Java Effective by Joshua Bloch:

Final fields

No internal field can be reassigned after construction. This is very important: once the object has been constructed, there is no way to modify it.

public final class Truck {
 private final String brand;
 private final String model;
  ...
}
Need professional services?
Rely on our Expertise

Final field have a special semantic in the JVM which is often overlooked, especially with code facing concurrency and race conditions. Final fields are:

  • Initialized at object construction: once the constructor exits, all final fields are set,
  • Safe to publish to other threads: the field value can be safely read without any synchronization.

These properties imply advantages that mutable objects don't have.

No mutators

No method allows to modify the objects' internal state. The example below shows a mutating method (known as a Setter). The truck's brand can be modified after construction.

public class Truck {
 private String brand;

 ...
 public void setBrand(final String brand) {
   this.brand = brand;
 }
 ...
}

Final Class

No one can extend the class. This prevents that someone adds any mutating fields, breaking the immutability. Immutable classes that cannot be extended are called strong immutable.

public final class Truck {
 // this class cannot be extended
}

Private fields

This prevents access to any mutable field reference. For example, a final Collection field could be modified if accessible from the outside. Truly immutable field references could be made public without any issue too.

Mutable internals are read-only

Mutable components are not accessible, or only in a read-only fashion.

Example

The following class is immutable:

public final class Truck {
 private final String brand;
 private final String model;

 public Truck(final String brand, final String model) {
   this.brand = brand;
   this.model = model;
 }

  ...
}

A Truck has a brand and a model. Like in the real world, once a truck has been built, it's not possible to change its brand or model. (or at least not easily..)

Real-world example

Have you recognized the bricks on the article image? Yes, Lego! Lego bricks are the perfect analogy to illustrate what an immutable object is and how it can be used to construct many things. Every single brick is an immutable object. You can't modify it, you can't break it. A single brick does nothing special by its own, but assemble it with many other bricks and you can create almost anything.

Building software with immutable objects is like assembling Lego bricks together.

Advantages over mutable objects

There are multiple advantages of using immutable instead of mutable objects.

Thread-safety

As the state of the object cannot be changed, it can be freely shared between threads. thread-safety is all about managing concurrency on mutable resources. As there is no mutability, there is no need for synchronization. Threads can concurrently access immutable objects without bothering with possible race conditions.

Immutable objects are always thread-safe. The Java Memory Model offers a special guarantee of initialization safety. In other words, a reference to an immutable object can be freely shared between as many threads as you want without synchronization.

No initialization hell

Suppose you have the following code:

public class TruckService {

 public void createRenaultTruck() {
   final Truck truck = new Truck();

   truck.setBrand("Renault");
   truck.setModel("K380");
   truck.putKeyInContact();
   truck.start();

   return truck;
 }
  ...
}

In the code above, the Truck class is mutable, thus anyone can modify the Truck instance. The biggest issue with this code is that the truck instance can be modified after construction by some wild code. This leads to bugs and maintainability issues.

Further code using the Truck may encounter unproper initialized Truck. It becomes very difficult to find which code should have initialized the truck properly, or if someone has modified it after construction.

It requires a lot of effort and research to find who initializes or modifies a mutable instance improperly. This complexity completely vanishes with Immutable objects: if the object is not properly initialized, it happened during construction. Find who constructs the object and you're done.

Safe use in all cases

Very strange bugs can happen when it comes to mutability. Let's take the following code as an example:

public class TruckService {

 public void doSomething() {
   final Truck truck = new Truck();
   truck.setBrand("Renault");
   truck.setModel("K380");

   final Map<Truck, String> map = new HashMap<>();
   map.put(truck, "Hello world");

   truck.setModel("K520");

   assert map.containsKey(truck); // returns false
 }
  ...
}

In the following example, the developer expect the map to contain the truck instance as key, as he put it in the map just before mutating it. The issue is subtle: when the truck is put as key in the map, it associates the truck hashcode (which depends on the truck fields) to the Hello World string. When changing the truck model, this changes the truck hashcode too. This is why the map cannot find the truck as key in the map anymore.

No need to copy or clone

As Immutable objects cannot be modified, there is no need to copy or clone its instance if you want to share it. Java Serializable and Cloneable contracts are known to be hard to understand.

Preconditions

Mutable objects can be partially or improperly initialized. This leads to cumbersome self-defending code like this to appear everywhere in the application:

public class TruckService {

 public void doSomething(final Truck truck) {
   if(truck.getBrand() != null) {
     if(truck.getModel() != null) {
       // I can finally focus on business code
     } else {
        // still no clue what to do...
     }
   } else {
     // what to do if not initialized correctly ?
   }
 }
  ...
}

Developers start to check every field if it's properly initialized instead of fixing the improper initialization. This known as Defensive Programming. Developers must defend their code from being defeated by invalid objects and check everything. This is exhausting. No one should need to defend itself from anyone else inside the same company. There are enough competitors outside the company to defend from.

This is completely against the Fail fast rule. In the example above, the code could also have failed due to a NullPointer when trying to manipulate the brand. In a Fail fast system, the issue would have been raised much earlier during object instantiation.

Bugs in software with mutable objects are harder to track and fix due to issues happening often way later than when it really happened. It leads to an insane number of If conditional checks on every field to protect the code from failing, instead of fixing the improper object construction.

On the other side, immutable objects can take advantage of Preconditions to protect from improper constructions:

public final class Truck {
 private final String brand;
 private final String model;

 public Truck(final String brand, final String model) {
   this.brand = Preconditions.checkNotNull(brand);
   this.model = Preconditions.checkNotNull(model);
 }
  ...
}

This way, everyone knows that Trucks have all a non-null brand and model. This makes all the if not null checks on the field unnecessary. The code can now focus on the business.

Invariants

Class invariants are the properties that an instance has during its entire lifetime even in case of mutating operations. For example, a collection size always reflects the number of elements inside the collection. If an element is added, the size is incremented.

Mutable objects can encounter Atomicity failure. For example, the following code can leave the mutable in an inconsistent state:

public class TruckService {

 public void doSomething(final Truck truck) {
   truck.setModel("");
   if(!truck.getBrand().isEmpty()) {
     throw new IllegalStateException("Truck brand already set");
   }
   truck.setBrand("");
 }
  ...
}

In this case, the truck model is reset to empty, but the truck is supposed to have an already empty brand. In this case, the truck is left modified with an empty model.

Better Performances

It shouldn't be the reason to adopt immutable objects, but it's something to know. The Garbage Collector (GC) can take advantage of unmodifiable objects since they don't need to be analyzed twice. The trickiest part in the Garbage Collector is to know whether an object has been modified or not. Since immutable objects can't be modified, the answer is obvious.

The GC is not the only one to have performance advantages when it comes to immutability. Jackson, a Json serializer, is several times faster serializing and deserializing immutable objects compared to mutable objects. Jackson heavily relies on Reflection to instantiate objects. Immutable objects can be instantiated in a single reflection call to the constructor.

@Value
@Builder
public final class DelayAction implements Action {
  @Wither
  String id;
  Thinktime thinkTime;
  boolean isEnabled;

  @JsonCreator
  DelayAction(
      @JsonProperty("id") final String id,
      @JsonProperty("thinkTime") final Thinktime thinkTime,
      @JsonProperty("enabled") final boolean isEnabled) {
    super();
    this.id = checkNotNull(id);
    this.thinkTime = checkNotNull(thinkTime);
    this.isEnabled = checkNotNull(isEnabled);
  }
}
Immutable class with Jackson constructor annotations

On the other side, Jackson needs to call each setter of a mutable instance to set all the fields. The more fields the object have, the more time it takes to Jackson to initialize the object.

New Development Patterns

Even if immutable objects have a great number of advantages, they require to have a new coding style.

Modification

Everytime you will need to modify an immutable object, you will need to create a new instance of it. Immutable objects doesn't mean you can't create modified instances of them!

public final class ImmutableTruck {
 private final String brand;
 private final String model;

 public ImmutableTruck(final String brand, final String model) {
   this.brand = Preconditions.checkNotNull(brand);
   this.model = Preconditions.checkNotNull(model);
 }
  ...

  public ImmutableTruck withModel(final String newModel) {
    return new ImmutableTruck(brand, newModel);
  }
}

Some developers will argue that it costs less to update an object rather than create a new one. This is wrong. If someone tells you this, they just wasted your time by mis-directing you to a problem which has been solved decades ago. There is a very interesting answer on Stackoverflow about if we should avoid creating objects.

Builders

Depending on the number of fields your immutable object has, when it exceeds 2 or 3 I would recommend using the builder pattern. Also, the builder pattern should not be an excuse to create objects with dozens of fields, even if it's immutable.

final Thinktime t = ThinktimeConstant
  .builder()
  .time(1)
  .unit(MINUTES)
  .build();

Builders have several advantages over constructor only objects when it comes to instantiation. The most obvious is that builders have named methods to initialize fields which constructors don't have.

With the Lombok Java library, it's very easy to automatically generate a builder for an immutable object: simply annotate the class with @Builder.

@Value
@Builder
public class Person {
  String firstname;
  String lastname;
}

Person johnSmith = Person
  .builder()
  .firstname("John")
  .lastname("Smith")
  .build();

This answers to people who complain that IDEs can generate getters and setters but not builders. No one should rely on IDE static code generation. Suppose you generate hashCode and equals methods for a mutable object, and then add a new field. You must remember to delete and regenerate those methods. If you can forget, you will forget. Lombok generates code during annotation processing which guarantees always up-to-date code.

Remember that builders are not thread-safe. But, who initializes an object from multiple threads at the same time?

Defensive copies

Each time you create an immutable object, you have to make sure to copy any possible mutable field before storing it, or on each time it's being asked:

public final class ImmutableTruck {
 private final Date builtOn;
 private final Collection<String> options;

 public ImmutableTruck(final Date builtOn, final Collection<String> options) {
   this.builtOn = new Date(builtOn); // must be copied to prevent caller from modifying it
   this.options = ImmutableList.copyOf(options); // guava's immutable list
 }

 public Date getBuiltOn() {
   return new Date(builtOn); // Date is mutable
 }

 public Collection<String> getOptions() {
   return options; // already immutable copy
 }

}

I strongly recommend to rely on immutable libraries like Joda-time to manage dates as the Java Date object is flawed. Joda time is an immutable date library for Java.

Immutable Collections

In the case you need to store collections in your immutable objects, I strongly recommend using Guava immutable collections. These collections are implementing the same interfaces as the Java collections but doesn't support any mutation. In almost all of our code, we usually only create a collection to be only read afterwards. We rarely need mutable collections, excepting as a local variable within a method.

If you are worried about performance, those immutable collections are mostly performing better than mutable Java ones.

False arguments against immutability

There are a number of invalid arguments against immutability:

  • It's impossible to design an entire immutable software: This is already known to be wrong, Octoperf has been designed with immutability from the beginning. Also look at the Scala language, this language has been designed to create immutable objects from the ground,
  • I need to modify objects, they must be immutable: how long in the lifetime of the object do you need to modify it? If it's a UI problem, making an object mutable because it's being modified half a second in its entire life is wrong. Just replace the instance with the modified one,
  • Object allocation is expensive: This is also known to be wrong as stated in the previous section.

Some people are just reluctant to change. Most legacy developers have been used to mutable objects and don't see why they need to change.

Improved productivity

With all these advantages (and manageable cons), immutable objects actually improve the developers productivity by decreasing their mental load. The system is easier to understand and to maintain. It's a great reason to make all your objects immutable.

Want to become a super load tester?
Request a Demo