Lombok Mutability Pitfalls and How To Avoid Them

Posted on December 31, 2017
Share article:

Find out how Lombok can unwittingly undermine good mutability and encapsulation practices and what to do about it. Object-oriented encapsulation best practices lead development towards only relying on mutators to change object state. Any other way of changing it is called a side-effect and is generally very unwelcome. Side-effects have a nasty habit of introducing bugs and seriously slowing down debugging as they tend to create non-determinism. That means that defects may occur randomly, depending on thread execution order.

In this article we will quickly take a look at how to protect our code from Lombok opening the door to side-effects.

For the purpose of this experiment, let’s define a Person class with the following interface:

public interface Person { 
    void setChildren(Set<Person> children); 
    Set<Person> getChildren();
}

A common Lombok implementation can look like this:

@Getter 
@Setter 
@AllArgsConstructor 
public class LombokPerson implements Person { 
    private Set<Person> children; 
}

1. Side-Effects Through Lombok @RequiredArgsConstructor and @AllArgsConstructor

Let’s define a main program that creates some people and works with them.

public static void main(String[] args){
    
    Set<Person> kidsOnKingsCrossRoad = new HashSet<>();

    Person joe = new LombokPerson(kidsOnKingsCrossRoad);
    
    //All good, Joe has no children yet
    Assert.assertEquals(0, joe.getChildren().size()); 

    //It happens that Hanna lives on KingsCrossRoad and she just had a child
    Person hannasChild = new LombokPerson(Collections.emptySet());
    kidsOnKingsCrossRoad.add(hannasChild);
    
    //Joe now magically has a child without knowing why
    Assert.assertEquals(1, joe.getChildren().size()); 
}

Because Lombok assigns the reference directly to the local field, when using @RequiredArgsConstructor and @AllArgsConstructor, the mutable type reference passed as parameter can be used to change the object’s state as a side-effect on the client side.

The solution to this issue is always manually implementing the constructors when there is at least one mutable type field, setting copies of the parameters to the local fields:

@Getter
@Setter
public class LombokPerson implements Person{
    private Set<Person> children;
    
    public LombokPerson(Set<Person> children){
        this.children = new HashSet<>(children);	
    }
}

Now, Joe can rest assured that new children born on his street won’t be mistakenly thought of as his because of constructor parameter side-effects.

2. Side-Effects Through Lombok @Getter

Let’s change the main program to use the Lombok generated getter for the children field:

public static void main(String[] args){
    
    Person joe = new LombokPerson(new HashSet<>());
    
    //Because Joe is the only one who lives there, 
    //we take a shortcut in computing kidsOnKingsCrossRoad 
    Set<Person> kidsOnKingsCrossRoad = joe.getChildren();

    //All good, Joe has no children yet
    Assert.assertEquals(0, joe.getChildren().size()); 

    //It happens that Hanna moves to KingsCrossRoad and she has a child
    Person hannasChild = new LombokPerson(Collections.emptySet());
    kidsOnKingsCrossRoad.add(hannasChild);
    
    //Joe now magically has a child without knowing why
    Assert.assertEquals(1, joe.getChildren().size()); 
}

Lombok returns the reference of the local field when implementing the getter method using @Getter, so kidsOnKingsCrossRoad and Joe’s children point to the same instance. That leads to side-effects whenever there are changes on that Set.

This can be avoided by always manually implementing getters for all mutable type fields to return copies, or immutable wrappers:

@Getter
@Setter
public class LombokPerson implements Person{

    @Getter(AccessLevel.NONE)
    private Set<Person> children;
    
    public LombokPerson(Set<Person> children){
        this.children = new HashSet<>(children);	
    }
    
    public Set<Person> getChildren(){
        return new HashSet<>(children);
        //Can also be Collections.unmodifiableSet(children), however
        //that will throw UnsupportedOperationException when attempting 
        //to mutate from the main program by calling Set#add
    }
}

Joe can be thankful that new parents moving in will not place their children in his care.

3. Side-Effects Through Lombok @Setter

We will now modify the main program to use the Lombok generated setter over the children field:

public static void main(String[] args){
    
    //Joe and Hanna are together but not married
    Person joe = new LombokPerson(Collections.emptySet());
    Person hanna = new LombokPerson(Collections.emptySet());
    
    //Joe and Hanna have a child together
    Set<Person> hannasChildren = new HashSet<>(); 
    hannasChildren.add(new LombokPerson(Collections.emptySet()));
    
    //Since they both have only this one child, 
    //we take a shortcut and assign the same set to both
    joe.setChildren(hannasChildren);
    hanna.setChildren(hannasChildren);
    
    //All good, Joe has only one child
    Assert.assertEquals(1, joe.getChildren().size()); 

    //It happens that Hanna adopts a child from foster care 
    Person adoptedChild = new LombokPerson(Collections.emptySet());
    hannasChildren.add(adoptedChild);
    
    //Joe now magically has an extra child without knowing why
    Assert.assertEquals(2, joe.getChildren().size()); 
}

As Lombok sets the parameter reference to the local field when implementing the setter method using @Setter, Joe and Hanna’s children fields point to the same instance after calling the setters. That leads to side-effects whenever there are changes on that Set.

To circumvent this, one must always manually implement setters for all mutable type fields to set copies of the actual parameters:

@Getter
@Setter
public class LombokPerson implements Person{

    @Getter(AccessLevel.NONE)
    @Setter(AccessLevel.NONE)
    private Set<Person> children;
    
    public LombokPerson(Set<Person> children){
        setChildren(children);	
    }
    
    public Set<Person> getChildren(){
        return new HashSet<>(children);
    }
    
    public void setChildren(Set<Person> children){
        this.children = new HashSet<>(children);	
    }
}

Joe and Hanna can now choose to have children separately without undue implications.

4. Side-Effects Through Lombok @Data

An example with @Data will reveal all the previously discussed flaws as it adds @Getter, @Setter and @RequiredArgsConstructor by default.

@Data
public class DataPerson implements Person{
    private Set<Person> children;

    public void addChild(Person child){
        children.add(child);
    }
}

public static void main(String[] args){
    
    Set<Person> kidsOnKingsCrossRoad = new HashSet<>();

    Person joe = new DataPerson(kidsOnKingsCrossRoad);
    
    //All good, Joe has no children yet
    Assert.assertEquals(0, joe.getChildren().size()); 

    //It happens that Hanna lives on KingsCrossRoad and she just had a child
    Person hannasChild = new DataPerson(Collections.emptySet());
    kidsOnKingsCrossRoad.add(hannasChild);
    
    //Joe now magically has a child without knowing why
    Assert.assertEquals(1, joe.getChildren().size()); 

    //Because Joe is the only one who lives there, 
    //we take a shortcut in computing kidsOnFirstFloor
    Set<Person> kidsOnFirstFloor = joe.getChildren();

    //Joe only has that one mysterious child found earlier
    Assert.assertEquals(1, joe.getChildren().size()); 

    //It happens that John rents a place on the first floor and he has a child
    Person johnsChild = new DataPerson(Collections.emptySet());
    kidsOnFirstFloor.add(johnsChild);

    //Now Joe has another mysterious child
    Assert.assertEquals(2, joe.getChildren().size()); 

    //The city administration department work to clear Joe’s situation
    Set<Person> emptyChildSet = new HashSet<>();
    joe.setChildren(emptyChildSet);

    //All good, Joe has no more children
    Assert.assertEquals(0, joe.getChildren().size()); 

    //Later, Joe adopts a child from foster care
    joe.addChild(new DataPerson(Collections.emptySet());

    //Now the administration department have an extra child somehow
    Assert.assertEquals(1, emptyChildSet.size());
}

To prevent all these headaches, always manually implement setters and getters for all mutable type fields to set and return copies of the actual parameters. Also, manually implementing the constructors to use the setters on mutable fields will prevent constructor parameter side-effects:

@Data
public class DataPerson implements Person{

    @Getter(AccessLevel.NONE)
    @Setter(AccessLevel.NONE)
    private Set<Person> children;
    
    public DataPerson(Set<Person> children){
        setChildren(children);	
    }
    
    public Set<Person> getChildren(){
        return new HashSet<>(children);
    }
    
    public void setChildren(Set<Person> children){
        this.children = new HashSet<>(children);	
    }

    public void addChild(Person child){
        children.add(child);
    }
}

5. Side-Effects Through Lombok @Value

For those who favor immutability, Lombok provides the @Value annotation which aims to create immutable types. Surely side-effects cannot happen with that, now could they?

Let’s define an “immutable” person as follows:

@Value
public class ImmutablePerson{
    Set<ImmutablePerson> children;
}

It’s time to let Joe decide is he likes this approach:

public static void main(String[] args){
    
    Set<ImmutablePerson> kidsOnKingsCrossRoad = new HashSet<>();

    ImmutablePerson joe = new ImmutablePerson(kidsOnKingsCrossRoad);
    
    //All good, Joe has no children yet
    Assert.assertEquals(0, joe.getChildren().size()); 

    //It happens that Hanna lives on KingsCrossRoad too and she just had a child
    ImmutablePerson hannasChild = new ImmutablePerson(Collections.emptySet());
    kidsOnKingsCrossRoad.add(hannasChild);
    
    //Joe now magically has a child without knowing why
    Assert.assertEquals(1, joe.getChildren().size()); 
    
    //The city administration department analyze weird child appearance cases,
    //so they work to remove Joe's wrongfully added child from his file	
    Set<ImmutablePerson> weirdChildSet = joe.getChildren();
    weirdChildSet.clear();
    
    //Because of a side-effect mishap, Kings Cross Road has a missing child now
    Assert.assertEquals(0, kidsOnKingsCrossRoad.size()); 
    //At least Joe is happy
    Assert.assertEquals(0, joe.getChildren().size()); 
}

Lombok @Value, adds both @Getter and @AllArgsConstructor on class level. That means that all recommendations regarding mutable type fields, apply here as well. So, a safer approach would be:

@Value
public class ImmutablePerson{
    
    @Getter(AccessLevel.NONE)
    Set<ImmutablePerson> children;
    
    public ImmutablePerson(Set<ImmutablePerson> children){
        this.children = Collections.unmodifiableSet(new HashSet<>(children));
    }

    public Set<ImmutablePerson> getChildren(){
        return new HashSet<>(children);
        //Or simply return children as it is an unmodifiable set, however
        //that will throw UnsupportedOperationException when attempting 
        //to call Set#clear in the main application
    }
}

Summary

Lombok generated code works great for POJOs, but when requiring thread-safe data types, one must watch out for side-effects over mutable type fields.

When having at least one mutable type field and using:

  1. @AllArgsConstructor (or RequiredArgsConstructor)
    – Remove the annotation and manually implement the constructor, assigning copies of the parameters to all mutable type fields
  2. @Getter
    – Manually implement the getters for all mutable type fields, returning a copy or an immutable wrapper over it
  3. @Value
    – This annotation applies @Getter and @AllArgsConstructor, thus all of the above are required to protect the code from side-effects
  4. @Setter
    – Manually implement the setters over all mutable type fields, assigning copies of the parameters to the local fields
  5. @Data
    – This annotation applies @Getter, @Setter and @RequiredArgsConstructor, so all of the above are required to protect the code from side-effects
Share article:

2 Replies to "Lombok Mutability Pitfalls and How To Avoid Them"

  • Tim Büthe
    February 5, 2018 (13:13)
    Reply

    Hi there,

    I think the title is very misleading! This has nothing to do with lombok I’d say. You talk about mutable collections and defensive copying of collections, but it’s not lombok specific at all. If you ask your IDE to generate Getter, Setters and Constructors for you, it will have the same effect.
    Secondly, all five points describing the same problem and don’t add a lot of value I would say.

    Sorry for the rant man, but I was pointed to your post by a colleague who vaguely remembered that he should not use lombok because of “some issues”.

    Please consider to clarify that this is not a lombok issue, otherwise you spread doubt in the developer community, at least with beginners. (Also, you might consider mentioning Guava’s immutable collections)

    regards,
    Tim

    • Cristian S.
      February 9, 2018 (17:51)
      Reply

      Hi Tim!

      Thanks for the comment! You are right, mutability issues are not something specific to Lombok, but not taking the time to understand how the library works will lead to these problems.

      Best regards,
      Cristian


What do you think? Share your thoughts!

+ 14 = 17