Reputation: 1612
Having a hierarchical model which uses inheritance and generics in collection, I have a compilation error related to capture conversion:
CaptureConversion.java:16: error: incompatible types: NodeModel cannot be converted to CAP#1
treeModel.getNodes().set(0, primaryNode);
^
where CAP#1 is a fresh type-variable:
CAP#1 extends PublicNodeModel from capture of ? extends PublicNodeModel
The only way I was able to surpass this error was via following conversion method:
private static <T extends PublicNodeModel> T convert(PublicNodeModel node) {
return (T) node;
}
This conversion looks quite silly to me, so my question is if there is some "correct" way how to deal with this kind of conversion in Java?
The model and code causing this error is following (a full runnable example on GitHub).
A simplified model (e.g. no getters/setters etc.):
class PublicTreeModel {
List<? extends PublicNodeModel> nodes = new ArrayList<>();
}
class TreeModel extends PublicTreeModel {}
class PublicNodeModel {}
class NodeModel extends PublicNodeModel {}
Usage causing the compilation error:
TreeModel treeModel = Fixture.createModel();
int index = Fixture.indexOfPrimaryNode(treeModel.getNodes());
NodeModel primaryNode = (NodeModel) treeModel.getNodes().get(index);
// following two lines won't compile
treeModel.getNodes().set(index, treeModel.getNodes().get(0));
treeModel.getNodes().set(0, primaryNode);
Usage with aforementioned fix (the convert()
method):
// this will compile & run
treeModel.getNodes().set(index, convert(treeModel.getNodes().get(0)));
treeModel.getNodes().set(0, convert(primaryNode));
Upvotes: 3
Views: 578
Reputation:
If you want to force the matter, one idea is to make a cast:
( (List<PublicNodeModel>)treeModel.getNodes()).set(0, primaryNode);
at a cost of an unchecked or unsafe operations
warning, but this is, of course, not recommanded. @Andy Turner's answer is the way to go.
Upvotes: -1
Reputation: 140534
class PublicTreeModel {
List<? extends PublicNodeModel> nodes = new ArrayList<>();
}
You're not supposed to be able to put anything into that list, other than literal null
. This type says "it's some subclass of PublicNodeModel
, I just don't know which": because the compiler doesn't know which subclass to allow, it prevents you adding any elements. You're only supposed to consume elements from this list, not provide elements to it.
In this specific case, you're effectively just swapping elements around in the list, so there's a simple solution:
TreeModel treeModel = Fixture.createModel();
int index = Fixture.indexOfPrimaryNode(treeModel.getNodes());
Collections.swap(treeModel.getNodes(), 0, index);
That is: even if you don't know the exact expected subtype of the elements in treeModel.getNodes()
, you know that the element you're trying to put back in was taken out of the list; so it's type-safe (or, at least, no less type-safe than it was already).
In a more general case, there are two other solutions: firstly, remove the wildcard, so that the list accepts any subclass of PublicNodeModel
:
class PublicTreeModel {
List<PublicNodeModel> nodes = new ArrayList<>();
}
This may not be acceptable if you actually want to be able to rely on the elements of nodes
being specifically NodeModel
s.
A second approach would be to add a type parameter to the class for the type of elements to hold in the list:
class PublicTreeModel<T extends PublicNodeModel> {
List<T> nodes = new ArrayList<>();
}
In the context of the code you show which has the compilation error, the second approach might become:
class TreeModel extends PublicTreeModel<NodeModel> {}
TreeModel treeModel = Fixture.createModel();
int index = Fixture.indexOfPrimaryNode(treeModel.getNodes());
NodeModel primaryNode = treeModel.getNodes().get(index);
treeModel.getNodes().set(index, treeModel.getNodes().get(0));
treeModel.getNodes().set(0, primaryNode);
Upvotes: 4