Home > Back-end >  Lombok Builder with Immutable lists?
Lombok Builder with Immutable lists?

Time:08-01

I have the following class with Lombok builder:

@Builder(toBuilder = true)
@Data
public class TaskSettings {
    private List<Task> tasks;
    
    public static class TaskSettingBuilder {
        private List<Task> tasks = new ArrayList<>();
        
        public TaskSettingBuilder longLivedTask(Duration duration) {
            // construct long-lived task
            tasks.add(new Task());
            return this;
        }

        public TaskSettingBuilder shortLivedTask(Duration duration) {
            // construct short-lived task
            tasks.add(new Task());
            return this;
        }
    }
}

I have a custom builder which can take info for different types of tasks, construct and appropriate task to add to the list. This works fine but when I use the toBuilder implementation, it uses the original list instead of copying it. So if I add something to the new instance, old instance would be updated:

TaskSettings ts = TaskSettings.builder()
                .longLivedTask(Duration.ofDays(365))
                .build();
TaskSettings ts2 = ts.toBuilder().shortLivedTask(Duration.ZERO).build();

ts would now have the short-lived task. I tried @Singular annotation but that generates builder method that accepts Task object rather than allowing me to construct it. I know I can move the tasking building to a builder of the Task object instead, but I would like to do it as part of the TaskSettings. How do I accomplish this?

CodePudding user response:

Either with @Singular, or by not using lombok. There's nothing magical about List if you aren't using @Singular. It's a field. Like any other.

If you aren't using @Singular, and your list objects aren't immutable (both of these are the case in your pasted snippet), then this question generalizes to this:

"toBuilder() makes a shallow clone. However, given that my object's fields are themselves mutable, I need a deep clone. How do I make lombok create a deep clone?"

The answer is: Lombok does not do that and there are no easy ways to make it do that; deep cloning in java land is in general quite the conundrum. There just aren't many easy ways to do it. It's either a lot of legwork (write copy constructors and clone() impls all over the place), or hackery (serialize it, then deserialize it), and has a lot of underspecced issues (is the point of the deep clone simply to avoid method callers messing with state they should not mess with, thus implying that immutable objects need not be deep-copied, for example).

Hence, a feature request to Project Lombok in the vein of: "Please make deep copies a thing I can turn on" is problematic. How would us maintainers of lombok possibly deliver on that request?

With @Singular, the list itself is now a known concept to lombok and lombok will act accordingly. It's still not a deep copy (i.e. if you have a @Singular List<List<String>>, you're still in trouble unless those inner lists are immutable), but the list WILL (or if not, should be, file a bug!) be copied when you use toBuilder() and add something to it (the list you get when using @Singular is immutable. No need to copy it if it didn't change).

I tried @Singular annotation but that generates builder method that accepts Task object rather than allowing me to construct it.

This sounds like you're making a mistake in how you're using the various lombok annotations here, or possibly, a bug in lombok. Can you elaborate? Because it sounds like the answer is simply 'toss @Singular at your tasks field - that's all you need. Except apparently you tried that and something didn't turn out the way you wanted.

I suggest a new question for this, with code snippets showing what lombok ends up doing and what you wanted it to do.

CodePudding user response:

You should check out @Builder.ObtainVia https://projectlombok.org/features/Builder

ObtainVia gives you the ability to create an instance with field value from other places like from another field or return of method

@Builder(toBuilder = true)
@Data
public class TaskSettings {
    @Builder.ObtainVia(method = "initTask") // important
    private List<Task> tasks;

    private List<Task> initTask() {
        return new ArrayList<Task>();
    }
//...
}

class Main {
    public static void main(String[] args) {
        TaskSettings ts = TaskSettings.builder()
                .longLivedTask(Duration.ofDays(365))
                .build();
        System.out.println(ts.getTasks().size()); // 1 
        TaskSettings ts2 = ts.toBuilder().shortLivedTask(Duration.ZERO).build();
        System.out.println(ts.getTasks().size()); // without ObtainVia print 2, with ObtainVia print 1
        System.out.println(ts2.getTasks().size());// without ObtainVia print 2, with ObtainVia print 1
    }
}



  • Related