深入理解Java中方法重载的实现原理

一、前言

  今天看《深入理解Java虚拟机》这本书的时候,看到了其中对方法重载(Overload)以及方法重写(Override)的原理讲解,顿时有了恍然大悟之感。这篇博客我就来参考书中的内容,讲一讲方法重载的实现原理。


二、正文

 2.1 什么是方法重载

  讲重载的实现原理之前,还是先来说一说什么是方法重载。Java中的每一个方法,都有自己的签名,或者也可以叫做标识,用来确认它的唯一性。在同一个类中,不能出现两个签名一样的方法。而方法的签名由什么组成呢?答案是方法名称 + 参数列表,也就是说,一个类中不允许出现两个方法名称一样,而且方法的参数列表也一样的方法(一个static,一个非static也不行)。知道上面的概念后,我们就可以定义方法重载了:在同一个类中,拥有相同方法名称,但是不同参数列表的多个方法,被称为重载方法,这种形式被称为方法的重载。例如下面几个方法,就是重载的方法,它们拥有相同的名称,但是参数列表不同:

void test(int a) {
    System.out.println("type int");
}

void test(String a) {
    System.out.println("type String");
}

void test(String arg1, int arg2){
    System.out.println("String + int");
}

void test(int arg1, String arg2){
    System.out.println("int + String");
}

  需要注意的是,参数列表的不同指的是参数的数量不同,或者在参数数量相同的情况下,相同位置的参数类型不同,比如上面最后两个方法,虽然参数都是一个String,一个int,但是位置不同,所以也是允许的。可以注意到,最后两个方法的参数名称都是arg1arg2,且位置相同,但是并不影响,因为方法的签名和参数的名称无关,只和类型有关。

  最后需要注意的一点是,返回值并不能作为方法的重载条件,比如下面两个方法:

// 无返回值
void test(int a) {
    System.out.println("type int");
}

// 返回值为int
int test(int a) {
    return a;
}

  若一个类中同时出现以下两个方法,将会编译错误,因为它们的方法名称+参数列表是一致的,编译器无法识别。为什么返回值不能作为重载的依据呢?很简单,因为我们调用方法时,并不一定需要接收方法的返回值,比如下面这行代码,对于上面两个方法都是适用的,编译器无法确定选择哪一个:

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

 2.2 如何选择调用哪一个重载方法

  当出现多个重载的方法时,编译器如何决定调用哪一个被重载的方法呢?相信很多人都知道,是根据调用方法时传递的实际参数类型来确定。比如说最开始列举的四个test方法,如果我们使用test(1),那将调用void test(int a)这个方法;如果我们使用test("aaa"),那将调用void test(String a)这个方法。这个应该很好理解,编译器在编译期间,根据调用方法的实际参数类型,就能够确定具体需要调用的哪一个方法。但是,这只是一种简单的情况,下面来看看一种稍微复杂的情况,即继承关系下的方法重载(看完后先猜猜输出结果):

public class Main {
	// 声明一个父类
    static class Human {
    }
    // 声明两个子类
    static class Man extends Human {
    }
    static class Woman extends Human {
    }

    // 三个重载方法,参数类型分别为以上三种类型
    static void sayHello(Human human){
        System.out.println("human say Hello");
    }
    static void sayHello(Man man){
        System.out.println("man say Hello");
    }
    static void sayHello(Woman woman){
        System.out.println("woman say Hello");
    }

    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        sayHello(man);
        sayHello(woman);
    }
}

  以上代码的输出结果如下:

human say Hello
human say Hello

  根据结果可以看到,最终都调用了参数为父类型MansayHello方法。这是为什么呢?这是因为对重载方法的选择,是根据变量的静态类型来确定的,而不是实际类型。比如代码Human man = new Man()Human就是变量man的静态类型,而Man是它的实际类型。我们都知道,在多态的情况下调用方法,会根据实际类型调用实际对象的方法,但是在重载中,是根据静态类型来确定调用哪一个方法的。在上面的代码中,manwoman对象的静态类型都是Human,所以都调用static void sayHello(Human human)方法。和调用重写方法不同,由于一个对象的静态类型在编译期间就可以确定,所以调用哪个重载方法是在编译期就确定好了,这叫静态分派,而调用重写的方法却要在运行时才能确定具体类型,这叫动态分派


 2.3 重载调用的优先级

  接下来,我们再来看一个更加复杂的情况,如下代码:

public class Test {

    static void sayHello(char arg) {
        System.out.println("hello, char");
    }

    static void sayHello(int arg) {
        System.out.println("hello, int");
    }

    static void sayHello(long arg) {
        System.out.println("hello, long");
    }

    static void sayHello(Character arg) {
        System.out.println("hello, Character");
    }

    static void sayHello(Serializable org) {
        System.out.println("hello, Serializable");
    }

    static void sayHello(Object arg) {
        System.out.println("hello, object");
    }

    static void sayHello(char... org) {
        System.out.println("hello, char...");
    }

    public static void main(String[] args) {
        sayHello('a');
    }
}

  上面对sayHello方法重载了七次,这七个重载方法都只有一个参数,但是参数的类型各不相同。在main方法中,我们调用sayHello方法,并传入一个字符'a',结果不出意料,输出如下:

"hello, char"

  这个结果应该不会有意外,毕竟'a'就是一个字符,调用参数为char的方法合情合理。接着,我们将sayHello(char arg)方法注释掉,再来看看运行结果:

"hello, int"

  当参数为char的方法被注释后,编译器选择了参数为int的方法。这也不难理解,这里发生了自动类型转换,将字符a转换成了它的Unicode编码(97),因此调用sayHello(int arg)是合适的。接着,我们将sayHello(int arg)也注释掉,看看输出结果:

"hello, long"

  这时候调用了参数类型为long的方法,也就是说这里发生了两次转换,先将a转换成int类型的97,再将97转换为long类型的97L,接着再调用相应的方法。上面的代码中我没有写参数为floatdouble的方法,不然这种转换还会继续,而顺序是char->int->long->float->double。但是不会被转换成byteshort,因为这不是安全的转换,byte只有一个字节,而char有两个字节,所以不行;而short虽然有两个字节,但是有一半是负数,char的编码不存在负数,所以也不行。好了,接下来我们将sayHello(long arg)也注释,看看结果:

"hello, Character"

  根据结果可以发现,这里发生了一次自动装箱,将a封装成了一个Character对象,然后调用了相应的方法。这也是合情合理的。然后,我们再注释sayHello(Character arg)方法,再次运行:

"hello, Serializable"

  先在这个结果就有一点迷惑了,这么连Serializable都行?这是因为Character类实现了Serializable接口,也就是说这里发生了两次转换,先将'a'封装成Character对象,再转型成为它的父类型Serializable。所以,当我们调用重载的方法时,如果不存在对应的类型,则编译器会从下往上,依次寻找当前类型的父类型,直到找到第一个父类型满足某一个重载方法为止,若直到最后都没有找到,就会编译错误。Character类实现了两个接口,一个是Serializable,一个是Comparable<Character>,如果同时存在这两个参数类型的重载方法,编译器将会报错,因为这两个类型是同级别的,不知道该选择哪一个。这种情况下,我们可以使用显示的类型转换,来选择需要调用的方法。好了,我们现在将sayHello(Serializable org) 也注释,看看结果:

"hello, object"

  可以看到,这时候调用了参数类型为Object的重载方法。这正好验证了我们上面说的结论——从下往上寻找父类型的重载方法,因为Object就是所有类的父类(除了Object本身)。然后,我们再注释sayHello(Object arg)

"hello, char..."

  可以看到,调用了可变参数类型的方法,这时候的a被当成了一个数组元素。所以,可变成参数类型的优先级是最低的。如果此时还有一个sayHello(int... org),则在注释完sayHello(char... org)后,将调用它,正好又对应上了我们前面说的 char->int->long->float->double的顺序,这个顺序在可变长类型中也适用。

  说到这里,我们应该能够明白,在方法调用有多个选择的情况下,编译器总是会根据优先级,选择最适合的那个。而关于这个优先级如何决定,可以去看看Java语言规范,其中对这部分做了详细规定。


三、总结

  说了这么多,最关键的一点还是:重载是根据变量的静态类型进行选择的。只要理解了这一点,对于重载也就很容易弄懂了。最后还要说一点,无论对重载理解有多么深刻,想最后一个例子中这样模棱两可的代码还是不要写为好,毕竟可(rong)读(yi)性(ai)太(da)差了。希望这篇博客对想要了解重载的人有所帮助吧。


四、参考

  • 《深入理解Java虚拟机》
posted @ 2020-03-18 19:32  特务依昂  阅读(2273)  评论(0编辑  收藏  举报