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
}
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.