Builder default values - once again

3,471 views
Skip to first unread message

Slawomir Nowak

unread,
Aug 15, 2014, 7:53:57 AM8/15/14
to project...@googlegroups.com
I know this was already discussed and I know I can create constructor or method and annotate it with @Builder.

But what if I have something like that:
public class Clazz {
   
private String val1 = "default";
   
private String val2 = "default2";
   
private Clazz2 val3 = Clazz2.DEFAULT;
   
private int val4 = 666;
}

Before building my object, I need to check all these parameters and set them to default values if necessary.

And now suppose I need to add another two fields to my class - both with default values.
At this point, I also need to fix my constructor/build method...

Couldn't lombok do this for me somehow?

Or maybe am I missing something and there is already built-in option for achieving this? I'm new to lambok project.

Cheers,
Slawek


Reinier Zwitserloot

unread,
Aug 18, 2014, 8:58:32 AM8/18/14
to project...@googlegroups.com
The problem is that builders should work particularly well with final fields, but assigning a value to a final field right in the field declaration locks in the value which means we'd have to rewrite those expressions (we can do that), but more to the point it now looks rather unnatural: Someone who isn't intricately familiar with exactly how @Builder works would assume that these fields are somehow just locked into the stated defaults for all instances ever created and will probably scratch their head wondering why the heck the fields even exist if they can only ever hold one value anyway.

That means we either have to introduce 'magic names', like so:

public class Clazz {
    
private final String val1;
    
private static final String val1_DEFAULT = "default1";
}

But I don't like magic strings. There's also no way for us to warn unless we establish that _ANY_ variable declaration whose name ends in '_DEFAULT' that isn't in fact a default for a @Builder someplace is worth a warning.

The alternative is a construct roel thought up. A defaults block. This was designed for giving method parameters default values and looks like this:

public void methodDef(String arg1, int arg2) {
   
defaults: {
        arg1 = "Default1";
        arg2 = 5;
    }

    // code of method here
}

But, if we can extend this idea to defaults for fields is unclear. There's such a thing as an initializer which looks similar and is legal as a member of a class directly, but you can't stick a label on those. It would also cause compiler errors (changing final variables, though the defaults block of methods has the same issue if the args are marked final, but that is a class of error that we can prevent (lombok runs before the semantic checks that produce those errors run, so we can fix the code in time to avoid them).

Lack of label really annoys me, though.

You can do this right now with the current version of lombok with the following construct:


@Builder
public
 class Clazz {
    
private final String val1;
    
    public static class ClazzBuilder {
        private String val1 = "Default1";
    }
}

What's going on here is the following rule: Builder will not create that which you already explicitly created, but it will bolt on whatever builder would have added to that thing if you haven't explicitly done so yourself. So, normally, @Builder would have generated an inner class called ClazzBuilder, but you already did this, so it'll skip that part. It also won't create a field named 'val1' in that builder because you did so. But it WILL put a method called 'val1' in there, as well as a 'build' method, and the builder() static method that creates a new instance of ClazzBuilder will also be generated in Clazz itself.

Slawomir Nowak

unread,
Aug 22, 2014, 1:50:47 AM8/22/14
to project...@googlegroups.com
Yeah, your solution seems better than manually validating parameters in constructor. But it still requires to produce some boilerplate code.

Have you consider some kind of field annotation? 
Something like:
@DefaultBuilderValue(value = "default")
private final String arg;

Not to mention this is probably a bad idea, is this even possible?
I mean - in a very simple case like above I'd imagine this could work. I doubt it is possible to assign, say, new instance of some class this way.

Cheers,
Slawek 

Reinier Zwitserloot

unread,
Oct 29, 2014, 10:07:13 AM10/29/14
to project...@googlegroups.com
We'd need 1 annotation for each compiler-constant type (@StringDefault, @IntegerDefault, etcetera), and the expression can only be a literal ('5' would work, but 'System.currentTimeMillis()' cannot). Too many caveats to be worth doing.

Martin Grajcar

unread,
Oct 31, 2014, 6:21:42 PM10/31/14
to project...@googlegroups.com
Someone who isn't intricately familiar with exactly how @Builder works would assume that these fields are somehow just locked into the stated defaults for all instances ever created and will probably scratch their head wondering why the heck the fields even exist if they can only ever hold one value anyway.

Then you could try to make it obvious... e.g. like

private @NotReally final String val1 = "default1";

or some better name like @MadeNonFinal. Or the other way round as Lombok already adds the final modifier somewhere.

private @FinalByLombok String val1 = "default1";

or maybe @FinalByBuilder, or @FinalWithADefault, or whatever?

Marius Kruger

unread,
Oct 31, 2014, 6:31:49 PM10/31/14
to project...@googlegroups.com
On 1 November 2014 00:21, Martin Grajcar <maaar...@gmail.com> wrote:
private @NotReally final String val1 = "default1";

or some better name like @MadeNonFinal. Or the other way round as Lombok already adds the final modifier somewhere.

private @FinalByLombok String val1 = "default1";

or maybe @FinalByBuilder, or @FinalWithADefault, or whatever?


or just @Final  which gives a hint as to what is up..
 
--
✝ Marius

Michiel Werk

unread,
Mar 18, 2015, 10:48:13 AM3/18/15
to project...@googlegroups.com
Hi,

If I look at the original class, I do not see any final fields. and in that case it is confusing (it took me some debugging to find out why my default value was gone). I understand that it can be solved by writing and annotating your own constructor, however, it would be nice when Lombok respects the defaults, and in my opinion it is possible..

Why does Lombok not generate these kind of builders?

public class Example {

   
private String val1 = "default";

   
public static class ExampleBuilder {
       
private Optional<String> val1 = Optional.empty();

       
public ExampleBuilder name(String val1) {
           
this.val1 = Optional.of(val1);
           
return this;
       
}

       
public Example build() {
           
Example example = new Example();
           
if (val1.isPresent()) {
                example
.val1 = val1.get();
           
}
           
return example;
       
}
   
}
}


The java 8 Optional is used in this example, for pre java 8 use, a Lombok own Optional could be used.
Generating a constructor is in this case not required anymore.

Michiel

Reinier Zwitserloot

unread,
May 15, 2015, 6:30:18 AM5/15/15
to project-lombok
We're not going to use optional for this, and we can't add our own because lombok doesn't want to be a runtime dep.

An AtomicReference<> is a fine standin for such stuff (the ref ITSELF is null if unset, and if explicitly set it to null, it's an AtomicReference instance, pointing at null).

Nevertheless, wrapping every field in a builder in that is kinda pricey. We'd rather go with the in my view much simpler and more straightforward solution: If you either (A) never set that value, or (B) you set it with null, then (C) you get the default. It is simply impossible to force it to null. Why add the complication?

The problem is that @Builder is a thing you put on static methods. Putting it on a class is syntax sugar. Still, it's very common syntax sugar; it's a case we'll revisit. It's on our radar.


 --Reinier Zwitserloot

--
You received this message because you are subscribed to the Google Groups "Project Lombok" group.
To unsubscribe from this group and stop receiving emails from it, send an email to project-lombo...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Martin Grajcar

unread,
May 15, 2015, 9:51:02 PM5/15/15
to project...@googlegroups.com
On Fri, May 15, 2015 at 12:29 PM, Reinier Zwitserloot <rei...@zwitserloot.com> wrote:
An AtomicReference<> is a fine standin for such stuff (the ref ITSELF is null if unset, and if explicitly set it to null, it's an AtomicReference instance, pointing at null).

 Nevertheless, wrapping every field in a builder in that is kinda pricey.

Simply using one bit or byte per field would be uglier, but faster and surely less memory consuming. As this field would get a dollar-name and be used in generated code only, the ugliness shouldn't matter.
 
We'd rather go with the in my view much simpler and more straightforward solution: If you either (A) never set that value, or (B) you set it with null, then (C) you get the default. It is simply impossible to force it to null. Why add the complication?

This doesn't work for primitives.

The implication (A) => (C) is fine, but (B) => (C) sounds extraordinary strange. I can imagine 

class X {
    @Final private String name = "42";
}

assert X.builder().name(null).build().name() == "42";

as an argument against using Lombok. I guess, a saner way would be to forbid assigning null to such fields in the builder. This leads to the (less general but) unsurprising behavior: 
  • Unassigned -> use the default
  • Once assigned -> the default is gone
No idea if this is really better.

The problem is that @Builder is a thing you put on static methods. Putting it on a class is syntax sugar. Still, it's very common syntax sugar; it's a case we'll revisit. It's on our radar.

I can't see any potential problem here. The direction SomeClass -> SomeBuilder doesn't apply for methods, but SomeBuilder -> SomeArgumentList looks exactly the same (and will stay so when methods get default arguments).

Martin Grajcar

unread,
May 17, 2015, 9:20:40 PM5/17/15
to project...@googlegroups.com
Another note on field defaults.

On Mon, Aug 18, 2014 at 2:58 PM, Reinier Zwitserloot <rein...@gmail.com> wrote:

That means we either have to introduce 'magic names', like so:

public class Clazz {
    
private final String val1;
    
private static final String val1_DEFAULT = "default1";
}

But I don't like magic strings.

I hope that nobody does.
 
The alternative is a construct roel thought up. A defaults block. This was designed for giving method parameters default values and looks like this:

public void methodDef(String arg1, int arg2) {
   
defaults: {
        arg1 = "Default1";
        arg2 = 5;
    }

    // code of method here
}

But, if we can extend this idea to defaults for fields is unclear. There's such a thing as an initializer which looks similar and is legal as a member of a class directly, but you can't stick a label on those.

You could resort to the defaults block as above but nested in an initializer block. But it's ugly and repetitive and verbose when compared to

@Final private field1 = "Default1";
@Final private field2 = 5;


This could work not only for Builder but in general. In any case, Lombok would
  • use the initializer expression
  • remove it
  • and add the final modifier.
I guess that @Final shows pretty clearly that the fields are gonna be final, but that the initialization expression is not the final word.
  • For a Builder, the meaning is clear: field defaults.
  • For a @NoArgsConstructor, the meaning is clear again: field defaults.
  • For an @AllArgsConstructor, the meaning is clear but different: just make the fields final, otherwise unused (there should be a warning if there's no feature using the defaults).
  • Concerning @RequiredArgsConstructor I'm rather unsure as the fields are (eventually) final (i.e., are required), but have defaults (i.e., are optional). Once this gets decided, one of the above applies.
For a Builder with fields which should not be final, I guess that honoring

private field1 = "Default1";

could be the way to go. I wouldn't require any annotation here as it's prone to forgetting (unlike with @Final, which has a meaning on its own). Actually, the logic is simple: No matter if the field is annotated, the Builder honors the default values provided in the initialization expression. And so does any constructor which doesn't override them.

Concerning the implementation, the Builder's fields could simply get these initialization expressions. The @NoArgsConstructor would move those annotated with @Final into its body and leave the others alone. The @AllArgsConstructor has nothing to do.
Reply all
Reply to author
Forward
0 new messages