javaintellij-ideajava-streamlombokbuilder

Stream map over classes with Lombok SuperBuilder


Class MyEntity extends a base class. Both have lombok's (version 1.18.34) @SuperBuilder annotation.

@Getter
@Setter
@SuperBuilder
@ToString(onlyExplicitlyIncluded = true)
@EqualsAndHashCode
public abstract class BaseEntity {

    public abstract SomeType getSomeType();
}
@Value
@SuperBuilder
@ToString(onlyExplicitlyIncluded = true)
@EqualsAndHashCode(callSuper = true)
public class MyEntity extends BaseEntity {

    @Override
    public SomeType getSomeType() {
        return SomeType.SOMETHING;
    }
}

If I try to map something to MyEntity like so

someItems.stream()
    .map(item -> MyEntity.builder().build())
    .toList();

The return type of this is List<?>. Why does the compiler fail to infer the type here? If I look at the build() method I do see a generic public abstract C build().
As a workaround, I can move the build to a separate method.

private MyEntity getBuild(SomeItem item) {
    return MyEntity.builder().build();
}


List<MyEntity> myEntities = someItems.stream() // works
    .map(item -> getBuild(item))
    .toList();

EDIT I took the classes I brought as example and created this example method. The type of the map list is List<?>

public static List<MyEntity> doSomething(List<String> strings) 
{
    List<?> list = strings.stream().map(s -> MyEntity.builder().build()).toList();
    return (List<MyEntity>) list;
}

Take a look at these:

public static List<MyEntity> creatingTheEntityFirst() {
    MyEntity build = MyEntity.builder().build();
    return List.of(build);
}

public static List<MyEntity> creatingEntityInAList(List<String> strings) {
    List<?> build = List.of(MyEntity.builder().build()); // type is List<?>
    return build; // this would error because provided is List<?>
    return List.of(MyEntity.builder().build()); // however this works
}

Solution

  • The return type of the toList call is not List<?>, but List<T> where T is a fresh type variable with upper bound MyEntity. This means that you can assign it to a variable of type List<? extends MyEntity>.

    // this compiles
    List<? extends MyEntity> result = someItems.stream()
        .map(item -> MyEntity.builder().build())
        .toList();
    

    The reason this happens is because builder() returns a MyEntityBuilder<?, ?>. Importantly, the first type parameter of this (the type C that is also the return type of build) is a wildcard, so we know nothing about C except that it inherits from MyEntity. MyEntityBuilder<?, ?> then undergoes capture conversion and the first ? becomes a fresh type variable with upper bound MyEntity. Let's call the fresh type variable T.

    This means that build would return T. So map returns a Stream<T>, and toList returns a List<T>.

    Since T has an upper bound of MyEntity, it is possible to convert T to MyEntity. But your code never does that before the T gets put into list, at which point it is no longer possible to do that implicitly (List<? extends MyEntity> is not a subtype of List<MyEntity>).

    You can do that T -> MyEntity conversion earlier, in the map lambda:

    List<MyEntity> result = someItems.stream()
            .map(item -> (MyEntity)MyEntity.builder().build())
            .toList();
    

    You might be wondering why builder is declared to return MyEntityBuilder<?, ?> as opposed to MyEntityBuilder<MyEntity, ?>. This is because the language rules simply does not allow this (JLS 8.4.8.3). Consider a concrete super class X and a concrete subclass Y, both having @SuperBuilder.

    The two classes would both end up having a builder method. X.builder would return XBuilder<X, ?> and B.builder would return YBuilder<Y, ?>. These are not return-type-substitutable methods.

    In your case, since the superclass is abstract, and lombok doesn't generate the builder method, it is possible for the subclass's builder to return MyEntityBuilder<MyEntity, ?>:

    @SuperBuilder
    class MyEntity extends BaseEntity {
        @Override
        public SomeType getSomeType() {
            return SomeType.SOMETHING;
        }
    
        public static MyEntityBuilder< MyEntity, ?> builder() {
            return new MyEntityBuilderImpl();
        }
    }
    

    Of course, this will not work if MyEntity has @SuperBuilder subclasses of its own.