Hi Sean,
it's fair to say that Java's immutability support at the language level is limited, especially compared to something like Clojure.
Sean Corfield wrote:How important do you feel immutability is in functional style code?
Personally, I find immutability quite important, regardless of the paradigm, as it eliminates so much possible bug surface.
The fewer moving parts, the better!
For functional programming, however, I see it as a must-have.
How do you balance that immutability with Java's mutable by default nature.
Designing immutable data structures in Java is possible, it just needs more work.
As Campbell already said, the introduction of Records (in Java 14 as a preview I think, finalized in 16?) gave us a new way to declare "data aggregation types", which makes it easier to design immutable types.
If you read the corresponding
JEP 395, they're supposed to "act as transparent carriers for immutable data".
Java is still only shallowly immutable, so some extra work is required to achieve "full" immutability, but it's the first step in the right direction.
You also need additional work when immutable data has to change.
Copy constructors, builder, withers, et.al., are possible, but you must either do it yourself or use a dependency.
At my company, we try to use Records for simpler types.
But for more complex types, especially if they can't be initialized in a single step, we rely on the
Immutables project, an annotation processor that generates immutable types based on abstract classes and interfaces, including builders, etc.
What advice do you have for trying to work extensively with immutability in Java?
My general advice, not only for immutability, is building a functional mindest, so you can reap many of the benefits of functional programming in any paradigm.
For immutability, that means for me:
Treat everything as immutable, especially if you don't control the API. For example, the Collectors.toList() returns usually an ArrayList, but it's not guaranteed as stated in the documentation, so don't rely on it.Prefer immutable types, like Records, over POJOs.Prefer pure functions/methods. Don't change incoming args.Wrap outgoing Collections in their unmodifiable wrapper.If it doesn't work as an immutable data structure, don't force it. There's no shame in mutability.
Still, you have to consider the potential overhead due to the lack of deep language-support.
Without persistent data structures with path copying, like in Clojure, you have to think about recreating/copying data structures and how it might affect your performance.
Today's hardware is way more powerful, and "premature optimization is the root of all evil".
But keeping it in mind from the beginning doesn't hurt, either.
That's why one viable approach to more FP in Java is the "Functional Core/Imperative Shell" design, which I talk about in the final chapter.
The imperative, mutable world is a layer around the "as pure as possible" and immutable inner core, that does the actual work.
Such FC/IS can be introduced gradually, you even can have multiple ones and size them as required.
I think one of the main lessons for a more functional approach to Java code is not being too strict, as the language was never designed to be the perfect functional language.
Every little new functional, or even only functional-akin feature, is still a very good thing in the long haul, because so many of us can't leave the Java world easily and switch to a "better for functional" language.
So you have to take the good with the bad, but I promise you that in the end, you still will be more productive and have safer, more straightforward code that's easier to reason with.