Java中方法重载的优先级

9

我知道这个问题已经被讨论了很多次,但我仍然不明白。

研究一下这段代码:

public class Main {  
    public static void var(Integer x, int y) {  
        System.out.println("Integer int");  
    }  


    public static void var(int... x) {  
        System.out.println("int... x");  
    }  

    public static void var(Integer... x) {  
        System.out.println("Integer...");  
    }  

    public static void main(String... args) {   
        byte i = 0;  
        Integer i2 = 127;  
        var(i, i2);  
    }  
} 

在我的大脑中遵循以下规则:
  1. 扩展

  2. 装箱

  3. 装箱+可变参数

根据这个规则,我采取了以下行动
1.将byte扩展为int
现在我有一个`int` `Integer`,存在一个方法可以接受`Integer`和`int`
2.进行装箱
因此。`int`->`Integer`和`Integer`->`int`参数
我认为这些参数是适用的,并期望看到它们。
Integer int

在输出中。
但我看到。
int ...

为什么?

我预计这段代码无法编译,看起来我的预期是正确的:http://ideone.com/Garnnp。 - Oliver Charlesworth
@OliCharlesworth 可能是JDK问题? - Boris the Spider
@OliCharlesworth 我也遇到了同样的错误。 - Rohit Jain
@gstackoverflow.. 你正在使用哪个Java版本进行编译? - Rohit Jain
@Rohit Jain 抱歉。我更新了主题。 - gstackoverflow
我在这里详细解释了这个问题:https://dev59.com/m2Ei5IYBdhLWcg3w0PCQ#20924662 - Jeroen Vannevel
2个回答

7
现在清楚的是被选中的方法是var(int...)而不是var(Integer...)。原因是只允许应用某些转换,而且只能从列表中选择其中一种转换,不能进行多个转换链。Java编译器不允许先进行扩展基本类型转换,再进行装箱转换。这在Java语言规范第5.3节中有说明。

5.3. 方法调用转换

方法调用转换应用于方法或构造函数调用中的每个参数值(§8.8.7.1,§15.9,§15.12):参数表达式的类型必须转换为相应参数的类型。

方法调用上下文允许使用以下之一:

  • 标识转换(§5.1.1)
  • 扩展原始类型转换(§5.1.2)
  • 扩展引用类型转换(§5.1.5)
  • 装箱转换(§5.1.7),后跟扩展引用类型转换
  • 拆箱转换(§5.1.8),后跟扩展原始类型转换(可选)。

编译器的唯一选择是:

  1. 对第一个参数进行扩展原始类型转换
  2. 对第二个参数进行拆箱转换

这将把(byte, Integer)转换为(int, int)

它不能先将第一个参数 byte 转换为 int,然后再将同一参数从 int 转换为 Integer 进行装箱转换,因为不允许连续进行两次转换。

让我们退回一步,了解编译器如何选择要调用的重载方法。这在 JLS 15.12.2 中有描述。(15.12.1 描述了如何查找要搜索的类或接口,但我们已经知道我们要调用类 Main 中的静态方法)

编译器选择正确的重载方法需要经历的前两个阶段不适用于可变参数("可变性") 方法,但第三个阶段适用:

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

第 15.12.4 节非常复杂,但适用于此处的规则是:

  • 首先应用非变量元数参数的规则(在您的情况下不适用)
  • 调用中的每个可变参数必须通过方法调用转换(上面我复制的那一部分)转换为可变参数声明的类型

所以..

  1. 你试图调用一个名为 var 的方法,参数为 (byte, Integer)
  2. 编译器查看你的方法 var(Integer...)
  3. 它会问:我能否将第一个参数,一个 byte,转换为 Integer(方法中声明的参数类型)
  4. 它查看 JLS 5.3 中的规则。它只能应用列表中的五个转换中的一个。其中没有一个可以直接将 byte 转换为 Integer - 它不能进行两个步骤。
  5. 所以编译器决定无法选择 var(Integer...)
  6. 然后它查看你的另一个方法,var(int...)
  7. 根据 JLS 5.3,它可以使用 扩展原始类型转换 将你的第一个参数,即 byte,转换为 int。那是一个勾号。
  8. 继续看第二个参数,你的 Integer,它发现 JLS 5.3 允许编译器使用 拆箱转换 将其转换为 int。因此这也是一个勾号。
  9. 那是最后一个参数,所以 var(int...) 是一个好的匹配。
  10. 编译器现在继续查看是否有更多与你的调用匹配的方法。如果有更多,那将导致模糊调用错误。
  11. 但是没有更多名称为 var 的方法,因此 var(int...) 是唯一适用的方法。编译器现在将生成代码来进行必要的转换并调用该方法。

这个解释对我来说够复杂的了。你能简单地描述一下这些动作吗? - gstackoverflow
好的,我会补充一些关于它如何选择要调用哪个方法的内容。 - Erwin Bolwidt
我认为这对大量的初学者开发人员很有帮助。 - gstackoverflow
1
不幸的是,有时事情并不那么清晰明了。如果你真的想知道为什么,你必须深入研究JLS。否则我的建议是:避免这些情况;只需用不同的名称命名你的两个方法。如果你花费很长时间去理解,下一个看代码的人也会花费很长时间,所以最好避免这些情况。 - Erwin Bolwidt
我正在努力理解你的回答,我需要时间来做到这一点。我正在准备SCJP考试。 - gstackoverflow
显示剩余3条评论

4
Java只能做“装箱和拆箱”,而不能做“拆箱和装箱”。例如,
• int -> Number[OK!] • int装箱为Integer扩展为Number • byte -> Integer[不编译] • byte需要先向int扩展,然后装箱成Integer。Java不允许这样做。请注意,您无法将byte装箱为Byte,然后扩展为Integer(Integer不是Byte的超类)。
因此,在您给出的方法中,第一个参数byte已经失败了两个整数方法。所以,只有int...可用。
我为演示编写了以下类:
public class Overload{

    public static void primitiveWiden(int x){
        System.out.println("int");
    }
    public static void refWiden(Map m){
        System.out.println("Map");
    }
    public static void priWideAndBox(Integer o){//doesn't work
        System.out.println("Object");
    }
    public static void boxAndRefWide(Number n){//it works
        System.out.println("Number");
    }

    public static void main(String[] args){
        byte b =0;
        int i =0;
        HashMap m = new HashMap();

        primitiveWiden(b);
        refWiden(m);
        priWideAndBox(b);//compile error
        boxAndRefWide(i);

    }

}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接