I am working with a code base that has relied on Eclipse for compilation until now. My objective is to compile it with javac
(via ant
) to simplify the build process. The project compiles without complaint in Eclipse (version 2019-12 (4.14.0)), but javac
(OpenJDK, both versions 1.8.0_275 and 14.0.2) produces method ... cannot be applied to given types
errors involving upper bounded wildcards.
Steps to reproduce
Note that the repository is 64 MB at time of writing:
git clone [email protected]:jamesdamillington/CRAFTY_Brazil.git
cd CRAFTY_Brazil && git checkout 550e88e
javac -Xdiags:verbose
-classpath bin:lib/jts-1.13.jar:lib/MORe.jar:lib/ParMa.jar:lib/ModellingUtilities.jar:lib/log4j-1.2.17.jar:lib/repast.simphony.bin_and_src.jar
src/org/volante/abm/agent/DefaultSocialLandUseAgent.java
Error message output:
src/org/volante/abm/agent/DefaultSocialLandUseAgent.java:166: error: method removeNode in interface MoreNetworkModifier<AgentType,EdgeType> cannot be applied to given types;
this.region.getNetworkService().removeNode(this.region.getNetwork(), this);
^
required: MoreNetwork<SocialAgent,CAP#1>,SocialAgent
found: MoreNetwork<SocialAgent,MoreEdge<SocialAgent>>,DefaultSocialLandUseAgent
reason: argument mismatch; MoreNetwork<SocialAgent,MoreEdge<SocialAgent>> cannot be converted to MoreNetwork<SocialAgent,CAP#1>
where AgentType,EdgeType are type-variables:
AgentType extends Object declared in interface MoreNetworkModifier
EdgeType extends MoreEdge<? super AgentType> declared in interface MoreNetworkModifier
where CAP#1 is a fresh type-variable:
CAP#1 extends MoreEdge<SocialAgent> from capture of ? extends MoreEdge<SocialAgent>
1 error
For completeness the method containing the offending line is:
public void die() {
if (this.region.getNetworkService() != null && this.region.getNetwork() != null) {
this.region.getNetworkService().removeNode(this.region.getNetwork(), this);
}
if (this.region.getGeography() != null
&& this.region.getGeography().getGeometry(this) != null) {
this.region.getGeography().move(this, null);
}
}
Analysis of error message
- We're told the compiler found type
MoreNetwork<SocialAgent,MoreEdge<SocialAgent>>
where it required MoreNetwork<SocialAgent,CAP#1>
.
- This implies the inferred value of
CAP#1
is incompatible with MoreEdge<SocialAgent>
- However we're told
CAP#1 extends MoreEdge<SocialAgent> from capture of ? extends MoreEdge<SocialAgent>
The Java documentation on Upper Bounded Wildcards states that
The upper bounded wildcard, <? extends Foo>
, where Foo
is any type, matches Foo
and any subtype of Foo
.
I cannot see why MoreEdge<SocialAgent>
doesn't match <? extends MoreEdge<SocialAgent>>
, and consequently cannot reconcile 2 and 3.
Efforts made to resolve the problem
While my objective is to compile for Java 8, I am aware that there have been bugs found in javac
related to generics and wildcards in the past (see discussion around this answer). However, I find the same problem with both javac 1.8.0_275 and javac 14.0.2.
I also thought about whether some form of explicit cast might provide the compiler with sufficient hints. However I can't think what to change since the type of this.region.getNetwork()
is reported as MoreNetwork<SocialAgent,MoreEdge<SocialAgent>>
in the error message as expected.
Generalisation of the problem (EDIT)
@rzwitserloot rightly pointed out that I hadn't included enough information about the dependencies in the code above to properly debug. Copying all the dependencies (including some code from libraries I don't control) would get very messy so I have distilled the problem into a self-contained program that produces an analogous error.
import java.util.HashMap;
import java.util.Map;
import java.util.List;
import java.util.ArrayList;
public class UpperBoundNestedGenericsDemo {
public static void main(String[] args) {
MapContainerManagerBroken mapContainerManager = new MapContainerManagerBroken();
MapContainer<A, B<A>> mapContainer = new MapContainer<>();
mapContainerManager.setMapContainer(mapContainer);
Map<A, B<A>> aMap = new HashMap<>();
aMap.put(new A(), new B<A>());
mapContainerManager.getMapContainer().addMap(aMap);
mapContainerManager.getMapContainer().removeMap(aMap);
}
}
/**
* Analogue of Region
*/
class MapContainerManagerBroken {
private MapContainer<A, ? extends B<A>> mapContainer;
void setMapContainer(MapContainer<A, ? extends B<A>> mapContainer) {
this.mapContainer = mapContainer;
}
MapContainer<A, ? extends B<A>> getMapContainer() {
return this.mapContainer;
}
}
/**
* Analogue of MoreNetworkService
*/
class MapContainer<T1, T2> {
List<Map<T1, T2>> listOfMaps = new ArrayList<>();
void addMap(Map<T1, T2> map) {
listOfMaps.add(map);
}
boolean removeMap(Map<T1, T2> map) {
return listOfMaps.remove(map);
}
}
class A {
}
class B<T> {
}
This compiles in Eclipse, but upon compiling with
javac -Xdiags:verbose UpperBoundNestedGenericsDemo.java
produces the error message
UpperBoundNestedGenericsDemo.java:18: error: method addMap in class MapContainer<T1,T2> cannot be applied to given types;
mapContainerManager.getMapContainer().addMap(aMap);
^
required: Map<A,CAP#1>
found: Map<A,B<A>>
reason: argument mismatch; Map<A,B<A>> cannot be converted to Map<A,CAP#1>
where T1,T2 are type-variables:
T1 extends Object declared in class MapContainer
T2 extends Object declared in class MapContainer
where CAP#1 is a fresh type-variable:
CAP#1 extends B<A> from capture of ? extends B<A>
UpperBoundNestedGenericsDemo.java:19: error: method removeMap in class MapContainer<T1,T2> cannot be applied to given types;
mapContainerManager.getMapContainer().removeMap(aMap);
^
required: Map<A,CAP#1>
found: Map<A,B<A>>
reason: argument mismatch; Map<A,B<A>> cannot be converted to Map<A,CAP#1>
where T1,T2 are type-variables:
T1 extends Object declared in class MapContainer
T2 extends Object declared in class MapContainer
where CAP#1 is a fresh type-variable:
CAP#1 extends B<A> from capture of ? extends B<A>
2 errors
Partial solution
The program in the previous section can be modified such that it compiles under both Eclipse and javac
by replacing MapContainerManagerBroken
with
class MapContainerManagerNoWildcards {
private MapContainer<A, B<A>> mapContainer;
void setMapContainer(MapContainer<A, B<A>> mapContainer) {
this.mapContainer = mapContainer;
}
MapContainer<A, B<A>> getMapContainer() {
return this.mapContainer;
}
}
That is, by removing the wildcard type bounds for MapContainer
. This solves the immediate practical problem, but limits the flexibility of MapContainerManagerNoWildcards
compared to MapContainerManagerBroken
. An alternative would be to make this class generic, e.g.
class MapContainerManagerFixedGeneric<T extends B<A>> {
private MapContainer<A, T> mapContainer;
void setMapContainer(MapContainer<A, T> mapContainer) {
this.mapContainer = mapContainer;
}
MapContainer<A, T> getMapContainer() {
return this.mapContainer;
}
}
However, this does not explain why the line mapContainerManager.getMapContainer().addMap(aMap)
is a compiler error when mapContainerManager
is a MapContainerManagerBroken
(such as in the example program). Specifically, why is the preceding line an error, but the following compiles?
MapContainer<A, ? extends B<A>> mapContainer = new MapContainer<A, B<A>>();