当前位置: 首页 > 面试题库 >

Java 8三元条件和未装箱原语的方法重载歧义

邢臻
2023-03-14
问题内容

以下是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从from int更改为Integer,则代码将在两个平台上编译。为什么发布的代码无法在Java 8中编译?


问题答案:

首先,让我们考虑一个没有三进制条件并且不能在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。编译时步骤2:确定方法签名

如果通过严格调用(§15.12.2.2),松散调用(§15.12.2.3)或可变arity调用(§15.12.2.4)之一应用方法,则该方法适用。

调用与调用上下文有关,这在JLS
5.3中进行了
说明。调用上下文

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

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

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

第二阶段(第15.12.2.3节)在允许装箱和拆箱的同时执行重载解析,但仍排除使用可变arity方法调用。
如果在此阶段未找到适用的方法,则处理将继续进行到第三阶段。

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

就我们而言,没有严格调用可应用的方法。两种方法都适用于松散调用,因为必须对double值进行装箱。

根据JLS
15.12.2.5,选择最具体的方法:

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

然后:

如果满足以下任一条件,则使用参数表达式e1,…,ek进行调用时,一个适用的方法m1比另一适用的方法m2更具体:

  1. m2是通用的,并且对于第18.5.4节,对于参数表达式e1,…,ek推断m1比m2更具体。

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个可变arity参数类型为S1,…,Sk,而m2的前k个可变arity参数类型为T1,…。对于所有i(1≤i≤k),自变量ei的类型Si比Ti更具体。另外,如果m2具有k
+ 1个参数,则m1个第k + 1个可变稀疏参数类型是m2个第k + 1个可变稀疏参数类型的子类型。

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

如果S <:T(第4.10节),则对于任何表达式,类型S都比类型T更具体。

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

根据$
4.10,在原始类型和类/接口类型之间没有定义子类型/超类型关系。因此,例如
int 不是 Object 的子类型。因此, int 不比 Object 更具体。

由于这两种方法之间没有 更多特定的方法,
因此也就没有
严格的更加特定的方法 ,也没有 最特定的 方法(JLS在同一段落JLS
15.12.2.5选择
最特定的 方法中给出了这些术语的定义)。因此,这两种方法都具有 最大的针对性

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

如果所有最大特定方法都具有等效的签名(第8.4.2节)…

这不是我们的情况,因此

否则,方法调用将是不明确的,并且会发生编译时错误。

根据JLS,我们这种情况下的编译时错误看起来是有效的。

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

在这种情况下,这两种方法仍然可以通过松散调用来应用。但是,由于Integer
<:Object,具有Integer参数的方法比具有2个Object参数的方法更具体。带有Integer参数的方法严格来说是更具体和最具体的,因此,编译器将选择它,而不会引发编译错误。

如果在此行中将double更改为Double,将会发生什么:double d = 1.0 ;?

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

正如Marco13在他的评论中指出的那样,本文讨论了类似的情况。为什么这种方法重载是模棱两可的?

如答案中所述,在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条件运算符吗?:和JLS
15.25.3。参考条件表达式。前一个将条件表达式分为3个子类别:布尔,数字和引用条件表达式。条件表达式的第二个和第三个操作数的类型分别为String和double。根据JLS,我们的条件表达式是参考条件表达式。

然后根据JLS
15.25.3。引用条件表达式我们的条件表达式是一个多引用条件表达式,因为它出现在调用上下文中。因此,我们的多条件表达式的类型是Object(调用上下文中的目标类型)。从这里开始,我们可以继续执行步骤,就好像第一个参数是Object一样,在这种情况下,编译器应选择int作为第二个参数的方法(并且不会引发编译器错误)。

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

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

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

从上面的解释中可以看出,当应用于重载方法时,JLS本身在与条件表达式有关的部分中是含糊不清的,因此我们必须做出一些假设。

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



 类似资料:
  • 本文向大家介绍Java Varargs中的方法重载和歧义,包括了Java Varargs中的方法重载和歧义的使用技巧和注意事项,需要的朋友参考一下 在Java中使用变量参数时存在歧义。发生这种情况是因为两种方法绝对可以有效地被数据值调用。因此,编译器不知道该调用哪种方法。 示例 输出结果 名为Demo的类定义了一个名为“ my_fun”的函数,该函数采用可变数量的浮点值。使用“ for”循环将这些

  • 问题内容: 我正在尝试制定以下方案中使用的规则。请解释为什么我得到2种不同的输出。 方案1的输出: 我是一个对象。 方案2的输出: 我是整数。 问题答案: 在Java语言规范说,这大约方法签名分辨率: 第一阶段(第15.12.2.2节)执行重载解析,不允许装箱或拆箱转换,也不允许使用可变Arity方法调用。如果在此阶段未找到适用的方法,则处理将继续进行到第二阶段。 在第二种情况下,涉及的方法签名适

  • 问题内容: 扩展和装箱Java原语。 我知道不可能将包装器类从一个扩展到另一个,因为它们不是来自同一继承树。为什么无法将原语扩展为另一种原语类型并自动装箱已扩展的原语? 假定可以将byte参数传递给需要int的方法,为什么不能将以下示例中的字节扩展为int然后装箱为Integer? 在上面的示例中,被编译器接受但被拒绝。 问题答案: 简短答案 Java语言仅支持某种程度的粗心。 更长的答案 我相信

  • 问题内容: 这会产生错误…为什么加宽和装箱均不执行叠加? 但是没有vararg的重载可以正常工作 在这里将执行add(long x),这将扩大拳击效果……为什么与var参数的概念不同 问题答案: Java编译器执行三种尝试来选择适当的方法重载(JLS§15.12.2.1): 阶段1:通过子类型识别匹配Arity方法 (可能的装箱转换和带有varargs的方法将被忽略) 阶段2:确定适用于方法调用转

  • 目前唯一稳定的创建Box的方法是通过Box::new方法。并且不可能在一个模式匹配中稳定的析构一个Box。不稳定的box关键字可以用来创建和析构Box。下面是一个用例: #![feature(box_syntax, box_patterns)] fn main() { let b = Some(box 5); match b { Some(box n) if n

  • 多路线电子小说 “根据选择或者其他操作,让游戏走向不同的剧情”是文字冒险游戏和一般的小说最大的区别之一。 例如恋爱游戏里,当某角色好感度高于90的时候,走向角色结局,否则就走向普通结局。 而实现这样效果的指令就是“条件分歧”,也就是“根据条件不同,执行不同的脚本段落”。 当好感度到达某个数值时…… 还有一种常见的剧情设定,就是在选择之后剧情并没有什么特别的变化,但是到了后面某个地方,突然之前埋下的