首页 > 解决方案 > javac 无法编译上限通配符,但 Eclipse 可以

问题描述

到目前为止,我正在使用一个依赖 Eclipse 进行编译的代码库。我的目标是用javac(via 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,MoreEdge<SocialAgent>>在需要的地方找到了类型MoreNetwork<SocialAgent,CAP#1>
  2. 这意味着推断的值CAP#1MoreEdge<SocialAgent>
  3. 然而我们被告知CAP#1 extends MoreEdge<SocialAgent> from capture of ? extends MoreEdge<SocialAgent>

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

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

我看不出为什么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

部分解决方案

可以修改上一节中的程序,使其在 Eclipse 下编译并javac替换MapContainerManagerBroken

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. 这解决了眼前的实际问题,但限制了MapContainerManagerNoWildcards比较的灵活性MapContainerManagerBroken。另一种方法是使此类通用,例如

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

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

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

标签: javaeclipsegenericsjavac

解决方案


您误解了关于? extends类型兼容性的含义的规则。

任何两次出现的? extends Number彼此都不相容,也不? extends NumberNumber自身相容。这是一个简单的“证明”,说明为什么会这样:

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

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

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

?与“想象我在这里使用了一个字母,而我只在此处使用该字母而没有在其他任何地方使用”相同。因此,如果您?涉及两个,它们可能彼此不相等。因此,出于类型兼容性的目的,它们是不兼容的。这是有道理的;想象你有:

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

那么很明显,重点是我可以调用这个传递一些List<Integer>a 和List<Double>b:?只要它符合界限,每个都可以成为它想要的任何东西。这也意味着不可能在这些列表中的任何一个上调用add,因为您添加的东西必须是 type ?,并且您不能做到这一点(除了,琐碎地,通过编写.add(null),因为 null 是用于此类目的的每种类型) ,但这不是很有用)。它还解释了为什么你不能写a = b;,这就是你问题的核心。为什么不能分配ab?毕竟,它们是同一类型!- 不,它们不是,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肯定听起来应该接受Objector? extends T和 not T。毕竟,arraylist 的.remove()方法接受任何对象,而不是T- 尝试从 int 列表中删除一些 double 根本不会做任何事情,根据规范:要求列表删除不在内部的东西是一个 noop。没有理由限制参数,那么。这解决了很多问题。


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

地图容器<A, ? 扩展 B> mapContainer = new MapContainer<A, B>();

因为这就是那个意思。记住,MapContainer<? extends Number>不代表一个类型。它代表了整个维度的类型。意思是这个mapContainer引用几乎可以指向任何东西,只要它是一个 MapContainer,任何“标签”(<>你喜欢的东西,只要介于两者之间的东西是 Number 或其任何子类型。您可以在这个爆炸的“可能是这么多东西”类型上调用的唯一方法是它可能在命令中拥有的所有可能的东西,并且没有addMap任何条带的方法是交集的一部分addMap方法的参数涉及一个A,如,在这个例子中,同样的事情? extends Number编译器说:嗯,我不知道。没有适合的类型。我不能去Number;如果你有一个MapContainer<Integer>?如果我让你addMap用 any打电话Number,你可以在里面放一个 Double ,这是不允许的。日食完全允许它的事实很奇怪。

这是一个边缘微不足道的例子:

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

在上面的例子中,不能在 3 个点上写任何东西,或者代替Amake that ever compile? extends是简写:没有添加/放置。时期。

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

一旦你完全理解了这一点,问题就得到了解释。更一般地说,鉴于你有一个MapContainer<? extends something>你不能在那件事上调用 addMap。时期。您不能对extends样式类型边界调用“写入”操作。

我已经解释了为什么它在这个答案的最上面。


推荐阅读