As laid out in the first piece, immutability is one of the core concepts of functional programming. Real functional programming languages support it by design, at a language-level. But in Java and most non-functional languages, we need to design and implement it ourselves, at code-level.
The fundamental idea behind immutability is simple: If we want to change a data-structure, we need to create a new copy of it with the changes, instead of mutating the original data-structure.
Why Immutability Matters
Why should we take the extra steps to change a value? Because with immutable data-structures, we gain a lot of advantages:
- Data won’t change behind our backs.
- Once verified, it will be valid indefinitely.
- No hidden side effects.
- No half-initialized objects, moving through different methods until it’s complete. This will decouple methods, hopefully making them into pure functions.
- Thread-safety: no more race conditions. If a data-structure never changes, we can safely use it in multiple threads.
- Better cacheability.
- Possible optimization techniques, like referential transparency and memoization.
State of Java Immutability
As of writing this article, Java isn’t a language with immutability by design. But it provides us with all the pieces needed to create immutable data-structures ourselves.
Built-in immutable types
There are various immutable types available in the JDK. Here are a few that you have probably already encountered:
- Primitive wrappers (
java.lang.String(except a lazy-calculated hash-code)
- Math types (
Java 8 Date/Time API
The “final” keyword
final` is used to define a variable that’s only assignable once.
It might sound like immutability at first, but it really isn’t in a broader sense.
Instead of creating an immutable data-structure, only the reference to it will be unchangeable. This only ensures that a variable is always pointing to the same point in memory. It says nothing about the memory content itself.
In March 2020, Java 14 will be released with a preview of records: “data holders” for shallowly immutable data.
Hopefully, we can replace many JavaBean types with records. Although there are multiple restrictions compared to traditional classes. They will make records safer to use, but we might have to rethink how we build our data-structures.
Java 14 is far into the future for many developers, myself included. I’m using Java 8 and, hopefully soon, 9. So let’s check out other options to achieve immutability today.
How to Become Immutable
There are effectively two ways of creating immutable data-structures without Java 14 records: doing it ourselves, or using a third-party-framework.
Think of a typical JavaBean:
JavaBeans are designed with getters and setters, so they can be used in various scenarios. Many frameworks, like ORM or GUI designers, are reflection-based. They analyze classes to identify getters and setters, not using fields via reflection directly, and are therefore mostly incompatible with immutable designs.
Another pitfall can be side effects. Setters might do more than just set a single value — they might track a dirty state or set multiple values. This isn’t best practice, but it happens all the time.
This particular JavaBean-design isn’t a must-have or fixed rule. It’s a convention that we often use thanks to habit-driven development, and not because we actually need that particular design.
Let’s break with traditional bean-design and make it immutable:
That was really simple and we got shorter, more concise code. Now our data-structure won’t change unexpectedly once it’s initialized.
Unless we add any more fields with a mutable type…
Every field of an immutable data-structure has to be immutable. And every field of the types of it has to be immutable, too. With a single mutable type, maybe even deeply nested, all the benefits of being immutable will be destroyed.
Collections are also problematic types. Unmodifiable collections have existed since Java 7.
With Java 9, easy-to-use factory methods were added.
But, just like
final, it only means the collection itself is unmodifiable, not the contained objects.
So be sure to only save already immutable data-structures in them.
We ensured all our fields are immutable, no matter how deeply nested they are. But we still have a problem: how to build the data-structure.
All fields must be set on initialization, so we need a constructor with all fields. What if we don’t need all the fields to be set? Should we provide multiple constructors or static builder-methods? And how do we ensure that all required fields are set and the resulting object is valid?
By using the builder pattern.
We need an additional class encapsulating the complex process of building an immutable data-structure. By separating the creation process from representation, we gain fine control over the data-structure assembly process, and can even add validation. It also means we introduce a mutable builder, so we can have an immutable data-structure.
Assume a more complex user type:
lastLogin are optional — by default a user isn’t active until explicitly stated and has never logged in. Either we provide the arguments every time we create a user, or we add additional constructors to match the different combinations of arguments.
But the more complex the type gets, the more constructors we would need.
Instead, we create a
Now we can build a user with a fluent API:
Or we can build it in multiple steps:
To make our builder even better, we can add validation to the methods or constructor — e.g. null-checks, or email-/password-validation — and throw an appropriate exception:
It’s good practice to increase encapsulation by putting the builder class directly into the corresponding type as a static nested class.
This way we can always use the type name
We successfully created an immutable type and a corresponding builder. But it’s a lot of boilerplate and it can be cumbersome to do that every time we introduce a new type. Every piece of code we write can introduce bugs.
That’s why using a third-party framework can be a real relief, saving typing all the code and providing a lot of convenience methods and validation.
Instead of writing all the code ourselves, we can employ third-party frameworks, to concentrate on designing and modeling the data-structures. The generated code is less error-prone and the resulting data-structures can be more concise.
As the project “Immutables” describes itself:
Java annotation processors to generate simple, safe and consistent value objects. Do not repeat yourself, try Immutables, the most comprehensive tool in this field!
Creating an immutable type is as simple as creating an abstract type, either an
abstract class or an
interface, and adding the right annotations:
The annotation processor generates the actual implementation behind the scenes, including:
- Serialization support.
- Builder class.
- Requirement validation.
- Convenience methods for copying, etc.
Let’s create an immutable user with the provided builder:
Thanks to using the
@Value.Default annotation and the
Optional type, we automatically get validation on calling
If not all the requirements are met, an
IllegalStateException is thrown.
Another convenience that the framework provides is adding helper methods for creating a copy of an immutable without the need to re-set all the values:
Or we can get a new builder to work with:
This is just the tip of the iceberg — the feature list is comprehensive:
- Strict builders:
Using a builder-method is allowed only once per builder.
- Derived attributes:
Read-only values derived from other attributes.
Add precondition checks, e.g. cross-validation.
- Auxiliary attributes:
Exclude an attribute from
- Style customization:
Many aspects of the generated code can be customized.
- JSON support:
Jackson and Guava.
- …and more
And of course immutables:
@Value is equal to using these quite self-explanatory annotations:
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
The only one missing is
@Builder, which adds
User.builder() to our example.
The project is a great tool for reducing boilerplate. But for building flexible immutable data-structures, I would recommend Immutables. Both projects have different goals, and I think it’s a given that a project named Immutables has better features regarding immutability.
Immutability is a good idea for many kinds of software projects, not just languages without built-in support. Even for data storage, it can provide advantages, like the version control system Git, for example. It uses immutable commits to ensure integrity.
But that doesn’t mean every problem is solvable with immutable data-structures. Mutable state isn’t a bad thing in itself. We just have to be sure when to use it and be aware of the pitfalls of changing state.
One disadvantage, at least at first, is adapting your codebase to the new data-structures. Changing them isn’t a drop-in replacement. But it helps us to create a more predictable data-flow, with easily observable state changes.
Our real-world example
We decided to start using immutables to simplify session management. Detecting a dirty session can be hard with deeply nested non-immutable data-structures. And persisting every session with every request creates a lot of unnecessary overhead.
Thanks to our new session data-structure, which only contains immutable types as fields, it’s much simpler to detect a changed session: If a field is updated, the session is dirty. No more changes to nested objects behind our backs.
But we didn’t stop at the session management. We’ve started to replace more and more types with immutables, to eliminate many subtle bugs, like invalid state or race conditions.
If a data-structure doesn’t change too much over its lifetime, we try to make it immutable.