Ahh, you missed the `lazy` keyword on there :-) Which is good because
it raises an issue: when you forget it, bad performance may result
without other observable consequence. Although, it's already the case
that reading code like the above ought to raise all kinds of alarm
bells (e.g., now I want to go check which fields computeHashCode()
might be referring to, and where /they're/ initialized), so I
/should/ be looking for that `lazy` keyword to put my mind at ease. So
maybe this is okay.
Well, "bad" is relative; it won't be any worse than what you do today
with eager static fields. But yes, I did drop the lazy there.
I assume that, unlike other field initializers, I'm safe to refer
to/any/ other field regardless of how and where that field is
initialized. Right?
I think you mostly are asking about instance fields. It would be safe
to refer to any other field, however, if you _read_ a lazy field in the
constructor, it might trigger computation of the field based on a
partially initialized object. The compiler could warn on the obvious
cases where this happens, but of course it can be buried in a chain of
method calls.
The intersection with primitives is interesting. I assume it gets
secretly created as an Integer? So there's a little extra hidden
memory consumption.
For static fields, there's an obvious and good answer that is optimally
time and space efficient with no anomalies: condy. We desugar
lazy static T t = e
...
moo(t)
into
// no field needed
static t$init() { return ; }
...
moo( ldc condy[ ... ] )
and let the constant pool do the lazy initialization and caching. JITs
love this.
For instance fields, we have a choice; use extra space in the object to
store the "already initialized" bit, or satisfy ourselves with the trick
that String does with hashCode() -- allow redundant recomputation in the
case where the initializer serves up the default value.
So I think the divide is not ref-vs-primitive but whether we are willing
to take the recomputation hit when it serves up a default value.