So, I am trying to create a collection of Builders which can be applied to a collection of Specifications to build Stages. I am having a problem with adding my generic builders to a collection so that they can later be used in the building process.
It has something to do with how generics are implemented in Java. I spent a few days trying different approaches, but so far have not come up with a solution to this problem. It feels like it should be possible, but I am missing something basic.
Full code here: https://github.com/apara/templateTrouble/blob/master/src/Main.java)
Here are the basic interfaces which are used in the code:
public interface Specifications {}
public interface Stage {}
public interface StageBuilder<SPEC extends Specifications, STAGE extends Stage> {
STAGE build(SPEC specifications);
boolean canBuild(SPEC specs);
}
After creating these basic interfaces, I have created some trivial implementations:
public class FilterSpecifications implements Specifications {}
public class GroupSpecifications implements Specifications {}
public class FilterStage implements Stage {}
public class GroupStage implements Stage {}
Finally, I implemented some trivial builders:
public abstract class AbstractStageBuilder<SPEC extends Specifications, STAGE extends Stage> implements StageBuilder<SPEC, STAGE> {
private Class<? extends SPEC>
specClass;
public AbstractStageBuilder(final Class<? extends SPEC> specClass) {
this.specClass = specClass;
}
@Override
public boolean canBuild(final SPEC specs) {
return
specClass.isAssignableFrom(specs.getClass());
}
}
public class FilterStageBuilder extends AbstractStageBuilder<FilterSpecifications, FilterStage> {
public FilterStageBuilder() {
super(FilterSpecifications.class);
}
@Override
public FilterStage build(final FilterSpecifications specifications) {
return
new FilterStage();
}
}
public class GroupStageBuilder extends AbstractStageBuilder<GroupSpecifications, GroupStage> {
public GroupStageBuilder() {
super(GroupSpecifications.class);
}
@Override
public GroupStage build(final GroupSpecifications specifications) {
return
new GroupStage();
}
}
The problem seems to be is that I am not sure what to do to build a collection of builders to pass into the build method:
public class Main {
public static void main(String[] args) {
//Create the builder list
//
final Collection<StageBuilder<Specifications,?>>
builders =
new LinkedList<>();
//*** THESE TWO LINES DO NOT COMPILE, cannot add specific builders to collection ***
//
//builders.add(new FilterStageBuilder());
//builders.add(new GroupStageBuilder());
final Collection<Stage>
result =
build(
builders,
Arrays.asList(
new FilterSpecifications(),
new GroupSpecifications()
)
);
System.out.println("Created stages: " + result.size());
}
static Collection<Stage> build(final Collection<StageBuilder<Specifications,?>> builders, final Collection <Specifications> specifications) {
return
specifications
.stream()
.map(
spec ->
builders
.stream()
.filter(
builder ->
builder
.canBuild(spec)
)
.findFirst()
.orElseThrow(
() ->
new RuntimeException(
"Builder not found for " + spec
)
)
.build(
spec
)
)
.collect(
Collectors.toList()
);
}
}
Thanks for any help.
PS: I don't necessarily want to cast, I am hoping ideally this could work with Java generics only.
EDIT 1:
I updated the list to the following specification:
final Collection<StageBuilder<? extends Specifications,? extends Stage>>
builders =
new LinkedList<>();
And now I am able to add the two builders, however, when sending the list to the build function:
static Collection<Stage> build(final Collection<StageBuilder<? extends Specifications,? extends Stage>> builders, final Collection <? extends Specifications> specifications) {...}
I get the following error in trying to invoke the builder.canBuild(spec) method:
Main.java:52: error: method canBuild in interface StageBuilder<SPEC,STAGE> cannot be applied to given types;
.canBuild(spec) //--- << unable to invoke here
^
required: CAP#1
found: CAP#2
reason: argument mismatch; Specifications cannot be converted to CAP#1
where SPEC,STAGE are type-variables:
SPEC extends Specifications declared in interface StageBuilder
STAGE extends Stage declared in interface StageBuilder
where CAP#1,CAP#2 are fresh type-variables:
CAP#1 extends Specifications from capture of ? extends Specifications
CAP#2 extends Specifications from capture of ? extends Specifications
So, it looks like it is "string wise" equivalent but both CAP#1 and CAP#2 while looking the same obviously are not. So, I need to somehow make them the same?
EDIT 2:
So, I got most of it working, but still cannot actually add the builder to a collection:
Builder collection is defined simply as:
final Collection<StageBuilder<Specification, Stage>>
builders =
new LinkedList<>();
The actual build function is defined as (no problems with it):
static
<STAGE extends Stage, SPEC extends Specification>
Collection<? extends Stage> build(final Collection<? extends StageBuilder<? super SPEC, ? super STAGE>> builders, final Collection <SPEC> specifications) {
return
specifications
.stream()
.map(
specification ->
builders
.stream()
.filter(
builder ->
builder
.canBuild(specification)
)
.findFirst()
.orElseThrow(
RuntimeException::new
)
.build(
specification
)
)
.collect(
Collectors.toList()
);
}
Here is where I am stuck. I created a helper function (as I seen done in some posts), and while it can be invoked, it fails to compile inside the function:
static
<SPEC extends Specification, STAGE extends Stage, BUILDER extends StageBuilder<?,?>>
void add(final Collection<? extends StageBuilder<? super SPEC, ? super STAGE>> builders, final BUILDER builder){ //StageBuilder<? super SPEC, ? super STAGE> builder) {
builders
.add(builder); // <-- DOES NOT COMPILE
}
Still getting one error:
Main.java:81: error: method add in interface Collection<E> cannot be applied to given types;
.add(builder); // <-- DOES NOT COMPILE
^
required: CAP#1
found: BUILDER
reason: argument mismatch; BUILDER cannot be converted to CAP#1
where SPEC,STAGE,BUILDER,E are type-variables:
SPEC extends Specification declared in method <SPEC,STAGE,BUILDER>add(Collection<? extends StageBuilder<? super SPEC,? super STAGE>>,BUILDER)
STAGE extends Stage declared in method <SPEC,STAGE,BUILDER>add(Collection<? extends StageBuilder<? super SPEC,? super STAGE>>,BUILDER)
BUILDER extends StageBuilder<?,?> declared in method <SPEC,STAGE,BUILDER>add(Collection<? extends StageBuilder<? super SPEC,? super STAGE>>,BUILDER)
E extends Object declared in interface Collection
where CAP#1 is a fresh type-variable:
CAP#1 extends StageBuilder<? super SPEC,? super STAGE> from capture of ? extends StageBuilder<? super SPEC,? super STAGE>
1 error
Here is the link to the current code for full reference:
https://github.com/apara/templateTrouble/blob/7356c049ee5c2ea69f371d3b84d44dbe7a104aec/src/Main.java
EDIT 3
Here is the essence of the problem, slightly changing the signature of the add method switches the problem between either not being able to add or to not being able to invoke the method.
So, we have either this setup:
//Attempt 1
//
add(builders, filterStageBuilder); // <-- DOES NOT COMPILE
add(builders, groupStageBuilder); // <-- DOES NOT COMPILE
static
<SPEC extends Specification, STAGE extends Stage, BUILDER extends StageBuilder<? extends SPEC, ? extends STAGE>>
void add(final Collection<? super BUILDER> builders, final BUILDER builder) {
builders
.add(builder); // <-- COMPILES FINE
}
Or this setup:
//Attempt 2
//
add2(builders, filterStageBuilder); // <-- COMPILES FINE
add2(builders, groupStageBuilder); // <-- COMPILES FINE
static
<SPEC extends Specification, STAGE extends Stage, BUILDER extends StageBuilder<? extends SPEC, ? extends STAGE>>
void add2(final Collection<? extends BUILDER> builders, final BUILDER builder) {
builders
.add(builder); // <-- DOES NOT COMPILE
}
So with final Collection builders I cannot invoke the method, but with final Collection builders I cannot add to the method.
The solution seems close at hand, but I cannot put my finger on the proper type specification that would allow me to invoke the method and add to the collection. This feels like it should be implementable without hackery such as casts.
EDIT 4
Very nice solution and explanation of the problem by Tim. I have updated the source code with the working, compiling code. For future references, here is the link:
https://github.com/apara/templateTrouble
-AP_