【问题标题】:Method overload ambiguity with Java 8 ternary conditional and unboxed primitivesJava 8 三元条件和未装箱原语的方法重载歧义
【发布时间】:2015-05-08 18:39:31
【问题描述】:

以下是在 Java 7 中编译的代码,但不是 openjdk-1.8.0.45-31.b13.fc21。

static void f(Object o1, int i) {}
static void f(Object o1, Object o2) {}

static void test(boolean b) {
    String s = "string";
    double d = 1.0;
    // The supremum of types 'String' and 'double' is 'Object'
    Object o = b ? s : d;
    Double boxedDouble = d;
    int i = 1;
    f(o,                   i); // fine
    f(b ? s : boxedDouble, i); // fine
    f(b ? s : d,           i); // ERROR!  Ambiguous
}

编译器声称最后一个方法调用不明确。

如果我们将f的第二个参数的类型从int更改为Integer,那么代码在两个平台上都可以编译。为什么发布的代码不能在 Java 8 中编译?

【问题讨论】:

  • 我 PC 上的 Java 8 版本 1.8.0_05 可以成功编译此代码,但 ideone 产生编译器错误,并且 ideone 似乎正在运行 Java 8u25。是否有最近的错误修复(或引入的错误)​​?
  • JLS 说一个值应该自动装箱或强制转换,但不能同时进行。如果您的编译器版本允许两者兼而有之,那么您可以期待未来的版本会发生变化。
  • @PeterLawrey 这会影响本案吗?我在代码中没有看到强制转换表达式。我从来没有尝试过将int 变成Double 之类的事情。
  • 条件运算符在这里重要吗?我们不能用f(boxedDouble, i); // fine f(primitiveDouble, i); // ERROR! Ambiguous 简化您的示例吗?
  • 如果我的编辑显着改变了您的问题,那么您可能应该回滚它。或者您可以将我的示例添加为问题的单独部分。

标签: java java-8 overloading


【解决方案1】:

让我们首先考虑一个没有三元条件且不能在 Java HotSpot VM(内部版本 1.8.0_25-b17)上编译的简化版本:

public class Test {

    void f(Object o1, int i) {}
    void f(Object o1, Object o2) {}

    void test() {
        double d = 1.0;

        int i = 1;
        f(d, i); // ERROR!  Ambiguous
    }
}

编译错误是:

Error:(12, 9) java: reference to f is ambiguous
both method f(java.lang.Object,int) in test.Test and method f(java.lang.Object,java.lang.Object) in test.Test match

根据JLS 15.12.2. Compile-Time Step 2: Determine Method Signature

如果方法适用于严格调用 (§15.12.2.2)、松散调用 (§15.12.2.3) 或可变参数调用 (§15.12.2.4) 之一,则该方法适用。

调用与调用上下文有关,此处解释为 JLS 5.3. Invocation Contexts

当方法调用不涉及装箱或拆箱时,将应用严格调用。当方法调用涉及装箱或拆箱时,将应用松散调用。

确定适用的方法分为三个阶段。

第一阶段(第 15.12.2.2 节)执行重载决议,不允许装箱或拆箱转换,或使用可变的 arity 方法调用。 如果在此阶段没有找到适用的方法,则处理继续到第二阶段。

第二阶段(第 15.12.2.3 节)执行重载决议,同时允许装箱和拆箱,但仍排除使用可变参数方法调用。 如果在此阶段没有找到适用的方法,则处理继续到第三阶段。

第三阶段(第 15.12.2.4 节)允许将重载与可变参数方法、装箱和拆箱相结合。

对于我们的例子,没有适用于严格调用的方法。这两种方法都可以通过松散调用来应用,因为必须将双精度值装箱。

根据JLS 15.12.2.5 Choosing the Most Specific Method

如果多个成员方法既可访问又适用于 方法调用,需要选择一个来提供 运行时方法分派的描述符。 Java 编程 语言使用选择最具体方法的规则。

然后:

一种适用的方法 m1 比另一种适用的方法更具体 方法 m2,用于使用参数表达式 e1、...、ek、if 的调用 以下任何一项都是正确的:

  1. m2 是通用的,m1 被推断为比 m2 更具体 §18.5.4 中的参数表达式 e1, ..., ek。

  2. m2 不是泛型,m1 和 m2 可以严格或宽松地适用 调用,其中 m1 具有形式参数类型 S1、...、Sn 和 m2 有形参类型 T1, ..., Tn,类型 Si 更具体 比所有 i (1 ≤ i ≤ n, n = k) 的参数 ei 的 Ti。

  3. m2 不是通用的,m1 和 m2 可通过变量arity 应用 调用,其中 m1 的前 k 个变量参数类型 是 S1, ..., Sk 和 m2 的前 k 个可变参数类型 是 T1, ..., Tk,对于参数 ei,类型 Si 比 Ti 更具体 对于所有 i (1 ≤ i ≤ k)。另外,如果 m2 有 k+1 个参数,那么 m1 的第 k+1 个变量参数类型是 m2的第k+1个变量arity参数类型。

上述条件是唯一一种方法可能比另一种方法更具体的情况。

如果 S <: t s>

看起来第二个条件匹配这种情况,但实际上并不匹配,因为 int 不是 Object 的子类型:int 对象。但是,如果我们在 f 方法签名中将 int 替换为 Integer,则此条件将匹配。请注意,方法中的第一个参数匹配此条件,因为 Object <:>Object 为真。

根据$4.10,基本类型和类/接口类型之间没有定义子类型/超类型关系。所以 int 不是 Object 的子类型。因此 int 并不比 Object 更具体。

由于在这两种方法中没有更具体的方法,因此不可能有更具体的方法,也不能有最具体的方法(JLS 在同一段 JLS 15.12.2.5 Choosing the Most Specific Method 中给出了这些术语的定义)。所以这两种方法都是最具体的

在这种情况下,JLS 提供了 2 个选项:

如果所有最具体的方法都具有重写等效签名(第 8.4.2 节)...

这不是我们的情况,因此

否则,方法调用不明确,出现编译时错误。

根据 JLS,我们案例的编译时错误看起来是有效的。

如果我们将方法参数类型从 int 更改为 Integer 会发生什么?

在这种情况下,这两种方法仍然可以通过松散调用来应用。但是,带有 Integer 参数的方法比带有 2 个 Object 参数的方法更具体,因为 Integer <: object integer>

如果我们将这一行中的 double 更改为 Double 会发生什么:double d = 1.0;?

在这种情况下,只有 1 个方法适用于严格调用:调用此方法不需要装箱或拆箱:f(Object o1, int i)。对于另一种方法,您需要对 int 值进行装箱,以便通过松散调用适用。编译器可以通过严格调用选择适用的方法,因此不会引发编译器错误。

正如 Marco13 在他的评论中指出的那样,这篇文章Why is this method overloading ambiguous?

中讨论了一个类似的案例

正如答案中所解释的,Java 7 和 Java 8 之间的方法调用机制发生了一些重大变化。这解释了为什么代码在 Java 7 中编译而不在 Java 8 中编译。


现在有趣的部分来了!

让我们添加一个三元条件运算符:

public class Test {

    void f(Object o1, int i) {
        System.out.println("1");
    }
    void f(Object o1, Object o2) {
        System.out.println("2");
    }

    void test(boolean b) {
        String s = "string";
        double d = 1.0;
        int i = 1;

        f(b ? s : d, i); // ERROR!  Ambiguous
    }

    public static void main(String[] args) {
        new Test().test(true);
    }
}

编译器抱怨方法调用不明确。 在执行方法调用时,JLS 15.12.2 没有规定任何与三元条件运算符相关的特殊规则。

但是有JLS 15.25 Conditional Operator ? :JLS 15.25.3. Reference Conditional Expressions。前者将条件表达式分为 3 个子类别:布尔、数字和参考条件表达式。我们的条件表达式的第二个和第三个操作数分别有 String 和 double 类型。根据 JLS,我们的条件表达式是一个参考条件表达式。

那么根据JLS 15.25.3. Reference Conditional Expressions,我们的条件表达式是一个多引用条件表达式,因为它出现在调用上下文中。因此,我们的 poly 条件表达式的类型是 Object(调用上下文中的目标类型)。从这里我们可以继续这些步骤,就好像第一个参数是 Object 在这种情况下编译器应该选择带有 int 作为第二个参数的方法(而不是抛出编译器错误)。

棘手的部分是来自 JLS 的注释:

它的第二个和第三个操作数表达式类似地出现在与目标类型 T 相同类型的上下文中。

由此我们可以假设(名称中的“poly”也暗示了这一点)在方法调用的上下文中,两个操作数应该被独立考虑。这意味着当编译器必须决定是否需要对此类参数进行装箱操作时,它应该查看每个操作数并查看是否需要装箱。对于我们的特定情况,String 不需要装箱,而 double 则需要装箱。因此,编译器决定对于这两个重载方法,它应该是一个松散的方法调用。进一步的步骤与我们使用双精度值代替三元条件表达式的情况相同。

从上面的解释看来,JLS 本身在应用于重载方法时,在与条件表达式相关的部分是模糊和模棱两可的,所以我们不得不做出一些假设。

有趣的是,我的 IDE (IntelliJ IDEA) 没有将最后一种情况(使用三元条件表达式)检测为编译器错误。它根据 JDK 的 java 编译器检测到的所有其他情况。这意味着 JDK java 编译器或内部 IDE 解析器都有错误。

【讨论】:

  • 根据这个答案,似乎这个问题和stackoverflow.com/questions/23020493/…...很相似?
  • @Marco13 你是对的。我错过了一件重要的事情:调用上下文。现在更新我的答案。
  • 您正在解释您自己的简化示例。不幸的是,这个例子与问题中的任何调用都不匹配。在这个问题中,没有调用有double 类型的第一个参数。引发错误的第三个调用具有第一个参数表达式b ? s : d,而sString。所以我怀疑b ? s : d 的类型是double(或任何其他原始类型。
【解决方案2】:

简而言之:

编译器不知道选择哪种方法,因为在选择最具体的方法方面,JLS 中没有定义原始类型和引用类型之间的顺序。

当您使用 Integer 而不是 int 时,编译器会选择带有 Integer 的方法,因为 Integer 是 Object 的子类型。

当您使用 Double 而不是 double 时,编译器会选择不涉及装箱或拆箱的方法。

在 Java 8 之前,规则不同,因此这段代码可以编译。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-08-29
    • 2011-10-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多