我所理解的重载与重载方法

Planeswalker23 2020年05月25日 共4,159字 127次浏览

本文旨在介绍 Java 重载、JVM 中的静态分派以及编译器选择重载方法的规则,希望能帮助到大家。

1. 重载

1.1 什么是重载

重载(Overload)就是指在同一个类中定义同名、不同参数类型或参数个数的方法。而重载方法的返回类型,可以相同也可以不相同。

或者我们可以说,重载就是指在一个类中的两个方法具有不同的方法签名。

The Java programming language supports overloading methods, and Java can distinguish between methods with different method signatures.
译文:Java 语言支持方法重载,它可以区分具有不同方法签名的方法。
原文来源:Defining Methods

1.2 方法签名

在 Oracle 官网文档中关于 Defining Methods 的文章中也定义了方法签名的描述。

Definition: Two of the components of a method declaration comprise the method signature—the method's name and the parameter types.
译文:方法声明的两个组件组成了方法签名——方法名和参数类型。

也就是说,一个方法的方法名和参数类型构成了它的方法签名。

对于一个类中的两个方法,如果它们具有相同的方法名和不同的方法签名,即拥有相同的方法名和不同的参数个数或不同的参数类型,那么它们就是重载方法。

1.3 方法重载示例

在日常开发中,我们使用重载最多的地方或许是一个类的构造方法,比如下面的 Person 类的两个构造方法就是重载方法。

public class Person {
    private String name;
    private int age;
    
    public Person() {
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

}

我们在创建一个 Person 对象的时候,可以通过传入不同的参数来完成创建的过程,实际上编译器会根据传入的参数选择合适的重载方法完成对象的创建。

2. 如何选择重载方法

我们已经知道编译器会根据传入的参数选择合适的重载方法来执行,那么下面的几个例子中编译器会选择哪个重载方法来执行呢?

2.1 方法重载示例2——声明类型

我在 OverloadDemo2 这个类中声明了两个重载方法 test,它们分别接收 Object 类和 String 类的参数。在 main 方法中,我又声明一个 Object 对象,并创建一个 String 对象赋值给它。

public class OverloadDemo2 {

    public static void test(Object o) {
        System.out.println("Object");
    }

    public static void test(String s) {
        System.out.println("String");
    }

    public static void main(String[] args) {
        Object obj = new String("1");
        test(obj);
    }
}

执行这个程序,结果输出了:Object。也就是说,对于一个声明为 Object 类但被赋予 String 类的对象,编译器在选择重载方法时会将它视为 Object 类。

这就告诉我们,在选取重载方法的时候,对于参数类型的判定是基于参数的声明类型的。

2.1.1 声明类型

为了介绍什么是声明类型,我们先来看一行代码。

Object obj = new String("1");

对于 obj 对象而言,它被声明为 Object 类,然后又创建了一个 String 类型的对象并赋值给 obj。在这其中,Object 类型被称为 obj 变量的声明类型(或静态类型),而 String 类型则被称为它的实际类型。

变量本身的声明类型是不会发生改变的,声明类型是在编译器就能确定的。而变量的实际类型在编译器是不确定的,在运行期才能确定。

编译器在重载时是根据参数的声明类型而不是实际类型作为判定依据的。我们可以通过字节码来验证这个结论。

public static void main(java.lang.String[]);
    Code:
       0: new           #6  // class java/lang/String
       3: dup
       4: ldc           #7  // String 1
       6: invokespecial #8  // Method java/lang/String."<init>":(Ljava/lang/String;)V
       9: astore_1
      10: aload_1
      11: invokestatic  #9  // Method test:(Ljava/lang/Object;)V
      14: return

上面通过javap -c 类名反编译命令得到的字节码就是主方法的字节码,看到第11行通过 invokestatic 指令调用了静态方法 test,而这个 test 方法的参数类型就是 Object。

示例代码中将 test 方法设置为静态方法,所以会用 invokestatic 指令,它的作用就是调用静态方法。
(Ljava/lang/Object;)V 是 test 方法的方法描述符,括号中的 Object 类是方法的参数,V 是指方法的返回值 void。

2.1.2 静态分派

现在我们已经知道编译器在编译时就可以确定要调用的重载方法,同时它是根据参数的声明类型来确定的。

我们又把所有依赖静态类型来定位方法执行版本的分派动作成为静态分派。

引自《深入理解 Java 虚拟机》(第二版)8.3.2 分派

2.2 方法重载示例3——继承与自动拆装箱

在了解了静态分派的概念后,接下来我们来看第三个示例代码,在 OverloadDemo3 类中,我声明了5个重载方法,并且在调用 test 方法时传入了一个 int 类型的值。

public class OverloadDemo3 {

    public static void test(int o) {
        System.out.println("int");
    }

    public static void test(Integer o) {
        System.out.println("Integer");
    }

    public static void test(int... s) {
        System.out.println("int...");
    }

    public static void test(Object s) {
        System.out.println("Object");
    }

    public static void main(String[] args) {
        test(1);
    }
}

当执行主方法后,输出的值是:int。这自然没有什么问题,传入 int 类型的参数,执行需要 int 类型参数的重载方法。

如果注释掉需要 int 类型参数的重载方法,再次运行程序,会发现这次的输出变成了:Integer。这是因为在编译时发生了自动装箱,int 类型的值被自动装箱成 Integer 类型,这样就会去调用需要 Integer 类型参数的重载方法。

如果注释掉需要 Integer 类型参数的重载方法,再次运行程序,会发现这次的输出变成了:Object。这是因为 Integer is a Objec,即参数又被转型成父类 Object 类型,从而找到了需要 Object 类型参数的重载方法。如果父类还有父类,那么将在继承关系中向上递归地去查找符合类型的重载方法。

如果注释掉需要 Object 类型参数的重载方法,再次运行程序,会发现这次的输出变成了:int...。这是因为编译器在进行自动装箱后也无法找到符合类型的重载方法,然后将参数转化为一个数组,即寻找可变长参数的重载方法。由此我们可以知道,可变长参数的重载优先级是最低的。

2.4 选取重载方法的规则总结

综上所述,我们可以得到编译器寻找重载方法的规则与优先级:

  1. 根据参数的声明类型(静态类型)寻找重载方法
  2. 考虑自动拆装箱
  3. 考虑参数的父类型
  4. 考虑可变长参数

3. 小结

  1. 重载和方法签名
  2. 选择重载方法的规则
  3. 对象的声明类型与实际类型