In our code base, we have several static or instance members that we would like to initialize lazily. If the initialization cannot yield null, It's easy to implement.
Otherwise, one could use an additional boolean
field that indicates whether the field is initialized, but that becomes noisy for many lazy initialized fields.
Another option is to make the field Optional
and use null
and Optional.empty()
to distinguish the uninitialized and initialized-to-null cases, but that is very ugly.
To improve this, I now consider using a Lazy<T>
type, which is constructed with a Supplier<T>
and has a T get()
method to retrieve the result, computing it on the first access only.
An implementation in Java can be found in Vavr.
For C#, there is an implementation of Lazy
in the system library.
Now I noticed that Vavr's implementation is marked as deprecated for the following reason:
Java isn't a lazy evaluated language. This implementation is ineffective because it is a wrapper. It does not scale well.
But isn't that the case for all functors and monads in Java? Should we stop using them all?
I understand that it's probably a bad idea wrapping every field in a Lazy
container, but having just a few of them should be fine, especially since the objects they wrap are typically large so the thin wrapper around will not make much difference in terms of memory usage.
So what is the best way to implement lazy initialization in Java? Are there better alternatives to Lazy
? Can someone elaborate why it was deprecated in Vavr?
1 Answer 1
There is nothing wrong with lazy initialization. In fact, that's a very common use case. I've written Lazy<T>
helpers myself, and lazy initialization is one of the better uses of the singleton pattern.
So do go ahead and use or create such a lazy initialization wrapper. This is much more elegant and much less bug-prone than manually checking whether an isInitialized
flag is set. Just be aware that there can be difficulties if multiple threads try to perform initialization.
Vavr seems to be targeted at general functional programming tools in Java, not just at useful utilities like lazy initialization. In some functional programming languages like Haskell, laziness allows for some nice tricks.
- For example, we don't strictly need an
if condition then a else b
"control flow" construct because function arguments are evaluated lazily. Instead, we could declare a function that can be called likeif cond a b
, and Haskell would only evaluate the correct argument. - We can also define an expression
undefined = undefined
(the value of the expression is the value of the expression) which would not work in a language like Java: perhaps a compiler would reject this, perhaps we'd get an initial value like null, or the computation wouldn't terminate and we'd get a stack overflow error. Not so in Haskell, where the compiler can ignore this recursion due to laziness. Here,undefined
is an expression that never evaluates to a value, and can be used as a placeholder anywhere! - Laziness is also super useful for other computations that don't ever terminate, e.g. infinite streams or fractals.
Using laziness in this manner wouldn't quite work in Java, because when you call a function in Java that function will be called, and infinite recursion will lead to a stack overflow. It seems that the Vavr authors don't want to propagate a style of programming that could easily lead to problems, so they've deprecated the Lazy type. But while lazy computation might not be a good fit for Java in general, it's typically still a very good fit for the more specific problem of initialization.
Stream
,Optional
,CompletionStage
etc. So while you can avoid them if you don't like them, they have become an important part of Java already. Also, the point of Vavr is to bring key Scala API to Java, which includes many more monads and functors such asEither
,Try
etc, so Vavr's philosophy cetrainly doesn't go against functors and monads.