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

Rob*_*per 24 java overloading java-8

以下是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
}
Run Code Online (Sandbox Code Playgroud)

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

如果我们改变的第二个参数的类型fintInteger,然后将代码编译两个平台上.为什么发布的代码不能在Java 8中编译?

med*_*088 16

让我们首先考虑一个没有三元条件且不在Java HotSpot VM上编译的简化版本(build 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
    }
}
Run Code Online (Sandbox Code Playgroud)

编译器错误是:

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
Run Code Online (Sandbox Code Playgroud)

根据JLS 15.12.2.编译时间步骤2:确定方法签名

如果适用于严格调用(第15.12.2.2节),松散调用(第15.12.2.3节)或变量调用(第15.12.2.4节)之一,则该方法适用.

调用与调用上下文有关,这在JLS 5.3中有解释.调用上下文

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

确定适用的方法分为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推断m1对于参数表达式e1,...,ek比m2更具体.

  2. m2不是通用的,m1和m2适用于严格或松散的调用,并且m1具有形式参数类型S1,...,Sn和m2具有形式参数类型T1,...,Tn,类型Si更多对于所有i,参数ei比Ti更具体(1≤i≤n,n = k).

  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个可变参数类型是第m + 1个可变参数类型m2的子类型.

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

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

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

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

由于在这两种方法中没有更具体的方法,因此没有严格的更具体的方法,也可能没有最具体的方法(JLS给出了同一段中JLS 15.12.2.5选择最具体方法的那些术语的定义).所以这两种方法都是最具体的.

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

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

因此,这不是我们的情况

否则,方法调用不明确,并发生编译时错误.

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

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

在这种情况下,两种方法仍然适用于松散调用.但是,使用Integer参数的方法比具有2个Object参数的方法更具体,因为Integer <: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);
    }
}
Run Code Online (Sandbox Code Playgroud)

编译器抱怨模糊的方法调用.在执行方法调用时,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"暗示这一点),在方法调用的上下文中,应该独立考虑2个操作数.这意味着当编译器必须决定是否需要对此类参数进行装箱操作时,它应该查看每个操作数并查看是否需要装箱.对于我们的具体情况String不需要拳击和双重将需要拳击.因此,编译器决定对于两个重载方法,它应该是一个松散的方法调用.进一步的步骤与我们使用双值而不是三元条件表达式的情况相同.

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

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

  • 根据这个答案,似乎这个问题非常类似于http://stackoverflow.com/questions/23020493/why-is-this-method-overloading-ambiguous ...? (2认同)