javac不能编译上界通配符,但Eclipse可以



到目前为止,我正在使用一个依赖Eclipse进行编译的代码库。我的目标是用javac(通过ant)编译它,以简化构建过程。该项目在Eclipse(版本2019-12(4.14.0))中编译时没有抱怨,但javac(OpenJDK,版本1.8.0_275和14.0.2)会产生涉及上界通配符的method ... cannot be applied to given types错误。

复制步骤

请注意,在撰写本文时,存储库为64MB:

git clone git@github.com: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

错误消息输出:

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

为了完整起见,包含违规行的方法是:

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);
}
}

错误消息分析

  1. 我们被告知编译器在需要MoreNetwork<SocialAgent,CAP#1>的地方找到了类型MoreNetwork<SocialAgent,MoreEdge<SocialAgent>>
  2. 这意味着CAP#1的推断值与MoreEdge<SocialAgent>不兼容
  3. 然而,我们被告知CAP#1 extends MoreEdge<SocialAgent> from capture of ? extends MoreEdge<SocialAgent>

关于上界通配符的Java文档指出

上界通配符<? extends Foo>,其中Foo是任何类型,与FooFoo的任何子类型匹配。

我不明白为什么MoreEdge<SocialAgent><? extends MoreEdge<SocialAgent>>不匹配,因此无法调和2和3。

为解决问题所作的努力

虽然我的目标是为Java 8进行编译,但我知道javac中过去曾发现过与泛型和通配符有关的错误(请参阅围绕此答案的讨论)。然而,我发现javac 1.8.0_275和javac 14.0.2都存在相同的问题。

我还考虑了某种形式的显式转换是否可以为编译器提供足够的提示。然而,由于this.region.getNetwork()的类型在错误消息中按预期报告为MoreNetwork<SocialAgent,MoreEdge<SocialAgent>>,我想不出该更改什么。

问题的概括(编辑)

@rzwitserloot正确地指出,我没有在上面的代码中包含足够的关于依赖关系的信息来正确地调试。复制所有的依赖项(包括我不控制的库中的一些代码)会变得非常混乱,所以我将问题提炼成了一个独立的程序,它会产生类似的错误。

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

这在Eclipse中编译,但在使用编译时

javac -Xdiags:verbose UpperBoundNestedGenericsDemo.java 

生成错误消息

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

部分解决方案

可以修改上一节中的程序,将MapContainerManagerBroken替换为,使其在Eclipse和javac下进行编译

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

也就是说,通过删除MapContainer的通配符类型边界。这解决了当前的实际问题,但限制了MapContainerManagerNoWildcardsMapContainerManagerBroken相比的灵活性。另一种选择是使此类通用,例如

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

然而,这并不能解释为什么当mapContainerManagerMapContainerManagerBroken时(例如在示例程序中)行mapContainerManager.getMapContainer().addMap(aMap)是编译器错误。具体来说,为什么前一行是错误的,而后面的行编译了?

MapContainer<A, ? extends B<A>> mapContainer = new MapContainer<A, B<A>>();
就类型兼容性而言,您误解了? extends的含义规则。

? extends Number的任何两次出现都不兼容,? extends Number也不兼容Number本身。以下是一个琐碎的"证据"来证明这一点:

List<Integer> ints = new ArrayList<Integer>();
List<? extends Number> numbers1 = ints; // legal
List<Number> numbers2 = numbers1; // legal?
numbers2.add(new Double(5.0)); // oh whoopsie

如果以上编译,那么ints中有一个非int,这是不好的。幸运的是,它没有编译。具体来说,第三行是编译器错误。

你所说的JLS的部分是关于单行道的。您可以将List<Number>分配给List<? extends Number>类型的变量(或者在参数为该类型时将List<Number>作为参数传递),但不能反过来。

?与"想象我在这里使用了一封信,而我只在这里使用这封信,没有在其他地方使用"相同。因此,如果涉及两个?,它们可能不相等。因此,出于类型兼容性的目的,它们是不兼容的。这是有道理的;想象一下你有:

void foo(List<? extends Number> a, List<? extends Number b>) {}

那么问题的关键是,我可以调用这个函数,为a传递一些List<Integer>,为b传递一些List<Double>:每个?都可以是它想要的任何东西,只要它符合边界。这也意味着,在这两个列表中的任何一个上调用add都是不可能的,因为您添加的东西必须是?类型,并且您无法实现这一点(除了通过编写.add(null),因为null是用于此类目的的所有类型),但这不是很有用)。它还解释了为什么你不能写a = b;,这也是你问题的核心所在。为什么不能将a分配给b?毕竟他们是同一类型的!-不,它们不是,CAP的东西捕捉到了这一点:a是CAP#1类型,b是CAP#2类型。这就是javac(大概还有ecj)解决这个问题的方法,这就是为什么会出现这种CAP的东西。这不是编译器故意密集或不足的问题。这是泛型复杂性所固有的。

因此,是的:CAP#1? extends Number不同,它只是其中的一个捕获(任何进一步的? extends Number都将被称为CAP#2,并且CAP#1和CAP#2不兼容;一个可能是Integer,一个可能毕竟是Double)。错误消息本身是合理的

通常,如果ecjjavac不一致,根据个人经验,通常ecj是正确的,而javac不是(我讲述了大约10次我遇到ecj和javac不一致的情况,10次中有9次,ecj比javac更正确;尽管我随后报告的JLS中经常存在歧义,并且已经解决)。尽管如此,考虑到JDK14仍然存在问题,并且试图解释这些错误消息(如果没有这里涉及的所有签名,这是非常困难的,您还没有粘贴代码库的有用部分),看起来javac确实是正确的。

通常的解决方法是在其中添加更多的?。特别是,removeEdge听起来确实应该接受Object? extends T,而不是T。毕竟,arraylist的.remove()方法接受任何对象,而不是T——根据规范,试图从int列表中删除一些double根本没有任何作用:要求列表删除不在里面的东西是徒劳的。那么,没有理由限制参数。这就解决了很多问题。


EDIT,在您用更详细的方式显著更新了您的问题之后。

MapContainer<A.扩展B>mapContainer=新的mapContainer<A、 B>();

因为这就是它的意思。请记住,MapContainer<? extends Number>并不代表类型。它代表了一个完整维度的类型。据说mapContainer是一个几乎可以指向任何东西的引用,只要它是一个MapContainer,任何"标记"的(请注意<>中的内容,只要介于两者之间的内容是Number或其任何子类型。在这个分解的"可能有这么多东西"类型上,您唯一可以调用的方法是它可以在命令中包含的ALL可能的东西,并且任何条纹的addMap方法都不是交集的一部分addMap方法的参数涉及A,在这个例子中,与? extends Number相同,编译器说:嗯,我不知道。没有适合的类型。我不能用Number;如果你有MapContainer<Integer>怎么办?如果我让你用任何Number调用addMap,你可以在那里放一个Double,这是不允许的。事实上,日食允许它,这是奇怪的。

这里有一个微不足道的例子:

Map<? extends Number, String> x = ...;
x.put(A, B);

在上面的例子中,任何东西都可以写在3个点上,或者代替A,使编译? extends是"无添加/放置"的简写。时期

实际上有一件事会起作用:x.put(null, B);,因为null"适合"每种类型。但这是一个copout,对严肃的代码根本没有用处。

一旦你完全理解了这一点,问题就会得到解释。更一般地说,假设您有一个MapContainer<? extends something>您就不能在那个东西上调用addMap。时期不能对extends样式的类型边界调用"write"操作。

我已经解释了为什么这是最重要的答案。

最新更新