zoukankan      html  css  js  c++  java
  • 深入理解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虚拟机》
  • 相关阅读:
    uva 12034 Race
    计算机基础之计算机硬件软件数据结构
    Project Perfect让Swift在server端跑起来-Perfect in Visual Studio Code (四)
    关于自己定义转场动画,我都告诉你。
    Shell编程入门
    CODEVS 1029 遍历问题
    【VBA研究】工作表自己主动筛选模式检測
    卸载MySQL 5.0
    操作系统(二)进程控制
    前端面试题
  • 原文地址:https://www.cnblogs.com/tuyang1129/p/12519578.html
Copyright © 2011-2022 走看看