javac 无法编译上限通配符,但 Eclipse 可以

javac cannot compile upper bounded wildcard but Eclipse can

我使用的代码库一直依赖 Eclipse 进行编译,直到现在。我的objective是用javac(通过ant)编译它来简化构建过程。该项目在 Eclipse(版本 2019-12(4.14.0))中编译无投诉,但 javac(OpenJDK,版本 1.8.0_275 和 14.0.2)产生 method ... cannot be applied to given types 错误涉及上限通配符。

重现步骤

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

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. 不兼容
  4. 但是我们被告知 CAP#1 extends MoreEdge<SocialAgent> from capture of ? extends MoreEdge<SocialAgent>

Java documentation on Upper Bounded Wildcards 表示

The upper bounded wildcard, <? extends Foo>, where Foo is any type, matches Foo and any subtype of Foo.

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

解决问题的努力

虽然我的 objective 是为 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 的通配符类型边界。这解决了眼前的实际问题,但与 MapContainerManagerBroken 相比限制了 MapContainerManagerNoWildcards 的灵​​活性。另一种方法是使这个 class 通用,例如

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 本身兼容。这是一个简单的 'proof' 原因:

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> 作为参数传递),但反之则不行。

? 等同于 'imagine I used a letter here and that I use this letter only here and nowhere else'。因此,如果涉及两个 ?,它们可能彼此不相等。因此,出于类型兼容性的目的,它们是不兼容的。这是有道理的;假设你有:

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

那么按理说,我可以调用这个传递一些 List<Integer> 给 a 和 List<Double> 给 b:每个 ? 都可以成为它想要的任何东西只要它符合界限。这也意味着 不可能 在这些列表中的任何一个上调用 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 根本不会做任何事情,按照规范:Asking a list to remove a thing那不在里面是一个空洞。那么没有理由限制参数。这解决了很多问题。


编辑,在你用更详细的方式显着更新你的问题之后。

MapContainer<A, ? extends B> mapContainer = new MapContainer<A, B>();

因为这就是那个意思。请记住,MapContainer<? extends Number> 不代表类型。它代表了整个维度的类型。它是说 mapContainer 是一个几乎可以指向任何东西的引用,只要它是一个 MapContainer,任何 'tag' 的东西(请 <> 中的东西,只要东西介于两者之间的是 Number 或其任何子类型。您可以在此展开的 'could be so many things' 类型上调用的唯一方法是 ALL 它可能在命令中拥有的东西,并且没有 addMap 任何条带的方法是交集 的一部分。addMap 方法的参数涉及 A,如在本例中,与 ? extends Number 相同,编译器说:好吧,我不知道。没有适合的类型。我不能使用 Number;如果你有一个 MapContainer<Integer>? 如果我让你使用任何 Number 调用 addMap,你可以在其中放置一个 Double,这是不允许的。eclipse 允许它的事实很奇怪。

这是一个边缘性的小例子:

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

在上面的例子中,nothing 可以写在 3 个点上,或者代替 A 使 ever compile? extends 是 shorthand 因为:否 add/put。期间.

实际上只有一种方法可以工作:x.put(null, B);,因为每种类型都为 null 'fits'。但这是一种逃避,对严肃的代码一点用处都没有。

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

我已经解释了为什么这是这个答案的最顶部。