None of the solutions are great.
Messing with constructors invisibly like this also feels bad. But it's probably the least evil choice. However, doing it right seems utterly impossible to me. I'm going to intentionally write some really ugly code to show off how hard this is going to be:
consider this as the base test case for all these:
@Builder @Value
class Example {
private static int counter = 1;
@Builder.Default int x = counter++;
}
CASE: How do you determine if the constructor _definitely_ sets the variable and therefore lombok should NOT inject 'this.x = $builder$defaultX();'?
Example:
consider:
public Constructor1A() {
if (Math.random() < .5) return;
this.x = 0;
}
versus
public Constructor1B() {
if (false) return;
this.x = 0;
}
versus
public Constructor1C() {
if (OtherType.SOME_PSF_BOOLEAN_FIELD_WITH_CONSTANT_VALUE_FALSE) return;
this.x = 0;
}
versus
public Constructor1D {
while (OtherType.someMethod()) {
doStuff();
}
this.x = 0;
}
versus
public Constructor1E {
while (OtherType.someMethod()) {
if (OtherType.foo()) return;
}
this.x = 0;
}
versus
public Constructor1F {
System.out.println(this.x);
while (OtherType.someMethod()) {
if (OtherType.foo()) return;
}
this.x = 0;
}
public Constructor1G {
do {
this.x = 0;
} while (false);
}
I have no idea what's even correct in some of these cases, and in other cases what I'd think is correct, is impossible for lombok to figure out:
1A: Presumably, lombok WILL generate 'this.x = $default()', and places it at the first line. If the dice fall such that x is later 0, counter has still been incremented, but that's more or less expected, I guess.... ? Or perhaps what I expect here is that upon hitting that 'return' statement, javac would have detected that, for this particular code path, this.x has definitely NOT been set, and therefore I expect lombok to silently upgrade that 'return' statement into first setting x. In other words, perhaps I expect counter to be incremented only half the time here. Most likely answer is that lombok detects that 'this.x' is not guaranteed to run and therefore lombok injects a this.x at start. Still, the point is: That's not quite as slam-dunk a case as I wanted it to be.
1B: Much harder; if(false) is defined explicitly in the java lang spec as being voodoo magic which does NOT trigger 'unreachable code!' errors, instead, it completely skips the body content; that return statement won't even be in the generated class file. I kinda expect lombok to act like javac does, and realize that in this particular case, the this.x assignment is guaranteed, and thus, I am not expecting counter to increase here. Lombok can, I guess, detect explicit if(false).
1C: ... whoops. No it can't, because javac can and will detect here that it's unreachable by way of explicit final. But lombok can't do that.
1D: I clearly expect lombok to NOT increment counter here; that statement is definite. The while body is convoluted and may exit abberantly (never finish, System.exit, or throw an exception), but then there is no object pointer anyway.
1E: ... but because a return statement shows up I now expect lombok to generate and increment counter at the start of the method.
1F: Okay, so here I expect the constructor to start out by printing '1' and not '0' (x has already been initialized with the default expr).. but if I later remove the 'return' statement in the while block, all of a suden.. it'll print... 0? That's kinda messed up, that a mix of whether or not there's a return statement in the constructor or a this.x assignment at the 'top level' someplace.
1G: This this.x is no longer at the top level but surely lombok can detect that it is guaranteed to run.. right? I expect no counter increment here.
Some more evil examples:
EvilConstructor() {
foo();
}
private void foo() {
this.x = 10;
}
@Builder.Default int x = counter++;
int greatEvil = foo(); // same foo as above
GreatEvil() {
//intentionally blank
}
In both cases, our eyeballs can trivially see that x will always be explicitly set, but in the EvilConstructor case, it is set during the constructor run, whereas with the GreatEvil case, it's set during the initializer run. If lombok generates a this.x = $def(); statement at the top of all explicit constructors, in one case x ends up being '10', but in the GreatEvil case, it'd end up being counter++, and not 10. What would I expect? Hard to say; I realize that lombok cannot possibly detect this and thus I expect lombok to generate the default, but the fact that in the GreatEvil case I still end up with x NOT being 10.. is very surprising to me!
I don't think this problem is solveable, so the real question is: Can we live with this clusterbomb, and file it away as 'oh come on nobody writes code THIS ugly, they deserve the mess!'? Should there be an opt-out mechanism? opt-out should probably be visible, so should the opt-out be.. on @Builder.Default itself? What if I want to opt-out but only for a specific constructor? Can I do something like annotate my explicitly written constructor with @Builder.Default.DontTouchMe?
Another example:
public class InstanceInit {
private static int counter = 1;
@Builder.Default private int x = counter++;
{
System.out.println(x);
}
}
}
If you remove the @Default annotation from the above code, that'd print 1. However, pretty much no matter how we're going to go about it, it would now print 0 with the @Default annotation. That's still 'surprising', but even if we were capable of detecting this scenario (which seems difficult to me), there isn't really anything we can do about it; there's no way to know if there's a point in pre-initializing x here, because that depends on which constructor is going to be invoked later. I guess we can detect that apparently it is going to matter so in this case, x should ALWAYS start out as resolving 'counter++' and becoming equal to the result, and if then later a constructor always overwrites x, so be it. Therefore, removal of the sysout line will change the behaviour (a constructor invocation would no longer increment counter, whereas with the print, it always does). This is a hairy situation: I have no idea what to expect. Pile on to that that it's hard to detect that this is happening. Imagine the instance initializer calls a method.