Functional Programming With Java: Immutability
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.
Table of Contents
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.Integer
,java.lang.Boolean
, etc.) java.lang.String
(except a lazy-calculated hash-code)- Math types (
java.math.BigInteger
,java.math.BigDecimal
) - Enums
java.util.Locale
java.util.UUID
Java 8 Date/Time API
The “final” keyword
The 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.
Java 14
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.
DIY immutables
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.
Builder pattern
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:
The fields active
and 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 builder
:
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 Builder
:
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.
Third-Party Frameworks
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.
Immutables
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.
equals
,hashCode
, andtoString
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 build()
.
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. - Preconditions:
Add precondition checks, e.g. cross-validation. - Auxiliary attributes:
Exclude an attribute fromequals
,hashCode
, andtoString
. - Style customization:
Many aspects of the generated code can be customized. - JSON support:
Jackson and Guava. - …and more
Project Lombok
Project Lombok is a comprehensive tool trying to reduce the amount of the usual Java boilerplate code, like getters and setters, null
checks, equals
/hashCode
, or toString
.
And of course immutables:
The annotation @Value
is equal to using these quite self-explanatory annotations:
@Getter
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
@AllArgsConstructor
@EqualsAndHashCode
@ToString
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.
Conclusion
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.
Resources
- Java Tutorial (Oracle)
- Java Records (Preview) (JEP 359)
- Introduction to Immutables (Baeldung)
- Introduction to Project Lombok (Baeldung)
- Why Immutables Are the Better Objects and How to Implement Them (Reflectoring)
- Immutables
- Project Lombok