一、前言
今天看《深入理解Java虚拟机》这本书的时候,看到了其中对方法重载(Overload
)以及方法重写(Override
)的原理讲解,顿时有了恍然大悟之感。这篇博客我就来参考书中的内容,讲一讲方法重载的实现原理。
二、正文
2.1 什么是方法重载
讲重载的实现原理之前,还是先来说一说什么是方法重载。Java
中的每一个方法,都有自己的签名,或者也可以叫做标识,用来确认它的唯一性。在同一个类中,不能出现两个签名一样的方法。而方法的签名由什么组成呢?答案是方法名称 + 参数列表,也就是说,一个类中不允许出现两个方法名称一样,而且方法的参数列表也一样的方法(一个static
,一个非static
也不行)。知道上面的概念后,我们就可以定义方法重载了:在同一个类中,拥有相同方法名称,但是不同参数列表的多个方法,被称为重载方法,这种形式被称为方法的重载。例如下面几个方法,就是重载的方法,它们拥有相同的名称,但是参数列表不同:
1 | void test(int a) { |
需要注意的是,参数列表的不同指的是参数的数量不同,或者在参数数量相同的情况下,相同位置的参数类型不同,比如上面最后两个方法,虽然参数都是一个String
,一个int
,但是位置不同,所以也是允许的。可以注意到,最后两个方法的参数名称都是arg1
和arg2
,且位置相同,但是并不影响,因为方法的签名和参数的名称无关,只和类型有关。
最后需要注意的一点是,返回值并不能作为方法的重载条件,比如下面两个方法:
1 | // 无返回值 |
若一个类中同时出现以下两个方法,将会编译错误,因为它们的方法名称+参数列表是一致的,编译器无法识别。为什么返回值不能作为重载的依据呢?很简单,因为我们调用方法时,并不一定需要接收方法的返回值,比如下面这行代码,对于上面两个方法都是适用的,编译器无法确定选择哪一个:
1 | public static void main (String[]args){ |
2.2 如何选择调用哪一个重载方法
当出现多个重载的方法时,编译器如何决定调用哪一个被重载的方法呢?相信很多人都知道,是根据调用方法时传递的实际参数类型来确定。比如说最开始列举的四个test
方法,如果我们使用test(1)
,那将调用void test(int a)
这个方法;如果我们使用test("aaa")
,那将调用void test(String a)
这个方法。这个应该很好理解,编译器在编译期间,根据调用方法的实际参数类型,就能够确定具体需要调用的哪一个方法。但是,这只是一种简单的情况,下面来看看一种稍微复杂的情况,即继承关系下的方法重载(看完后先猜猜输出结果):
1 | public class Main { |
以上代码的输出结果如下:
1 | human say Hello |
根据结果可以看到,最终都调用了参数为父类型Man
的sayHello
方法。这是为什么呢?这是因为对重载方法的选择,是根据变量的静态类型来确定的,而不是实际类型。比如代码Human man = new Man()
,Human
就是变量man
的静态类型,而Man
是它的实际类型。我们都知道,在多态的情况下调用方法,会根据实际类型调用实际对象的方法,但是在重载中,是根据静态类型来确定调用哪一个方法的。在上面的代码中,man
和woman
对象的静态类型都是Human
,所以都调用static void sayHello(Human human)
方法。和调用重写方法不同,由于一个对象的静态类型在编译期间就可以确定,所以调用哪个重载方法是在编译期就确定好了,这叫静态分派,而调用重写的方法却要在运行时才能确定具体类型,这叫动态分派。
2.3 重载调用的优先级
接下来,我们再来看一个更加复杂的情况,如下代码:
1 | public class Test { |
上面对sayHello
方法重载了七次,这七个重载方法都只有一个参数,但是参数的类型各不相同。在main
方法中,我们调用sayHello
方法,并传入一个字符'a'
,结果不出意料,输出如下:
1 | "hello, char" |
这个结果应该不会有意外,毕竟'a'
就是一个字符,调用参数为char
的方法合情合理。接着,我们将sayHello(char arg)
方法注释掉,再来看看运行结果:
1 | "hello, int" |
当参数为char
的方法被注释后,编译器选择了参数为int
的方法。这也不难理解,这里发生了自动类型转换,将字符a
转换成了它的Unicode
编码(97),因此调用sayHello(int arg)
是合适的。接着,我们将sayHello(int arg)
也注释掉,看看输出结果:
1 | "hello, long" |
这时候调用了参数类型为long
的方法,也就是说这里发生了两次转换,先将a
转换成int
类型的97
,再将97
转换为long
类型的97L
,接着再调用相应的方法。上面的代码中我没有写参数为float
和double
的方法,不然这种转换还会继续,而顺序是char->int->long->float->double
。但是不会被转换成byte
和short
,因为这不是安全的转换,byte
只有一个字节,而char
有两个字节,所以不行;而short
虽然有两个字节,但是有一半是负数,char
的编码不存在负数,所以也不行。好了,接下来我们将sayHello(long arg)
也注释,看看结果:
1 | "hello, Character" |
根据结果可以发现,这里发生了一次自动装箱,将a
封装成了一个Character
对象,然后调用了相应的方法。这也是合情合理的。然后,我们再注释sayHello(Character arg)
方法,再次运行:
1 | "hello, Serializable" |
先在这个结果就有一点迷惑了,这么连Serializable
都行?这是因为Character
类实现了Serializable
接口,也就是说这里发生了两次转换,先将'a'
封装成Character
对象,再转型成为它的父类型Serializable
。所以,当我们调用重载的方法时,如果不存在对应的类型,则编译器会从下往上,依次寻找当前类型的父类型,直到找到第一个父类型满足某一个重载方法为止,若直到最后都没有找到,就会编译错误。Character
类实现了两个接口,一个是Serializable
,一个是Comparable<Character>
,如果同时存在这两个参数类型的重载方法,编译器将会报错,因为这两个类型是同级别的,不知道该选择哪一个。这种情况下,我们可以使用显示的类型转换,来选择需要调用的方法。好了,我们现在将sayHello(Serializable org)
也注释,看看结果:
1 | "hello, object" |
可以看到,这时候调用了参数类型为Object
的重载方法。这正好验证了我们上面说的结论——从下往上寻找父类型的重载方法,因为Object
就是所有类的父类(除了Object
本身)。然后,我们再注释sayHello(Object arg)
:
1 | "hello, char..." |
可以看到,调用了可变参数类型的方法,这时候的a
被当成了一个数组元素。所以,可变成参数类型的优先级是最低的。如果此时还有一个sayHello(int... org)
,则在注释完sayHello(char... org)
后,将调用它,正好又对应上了我们前面说的 char->int->long->float->double
的顺序,这个顺序在可变长类型中也适用。
说到这里,我们应该能够明白,在方法调用有多个选择的情况下,编译器总是会根据优先级,选择最适合的那个。而关于这个优先级如何决定,可以去看看Java
语言规范,其中对这部分做了详细规定。
三、总结
说了这么多,最关键的一点还是:重载是根据变量的静态类型进行选择的。只要理解了这一点,对于重载也就很容易弄懂了。最后还要说一点,无论对重载理解有多么深刻,想最后一个例子中这样模棱两可的代码还是不要写为好,毕竟可(rong)读(yi)性(ai)太(da)差了。希望这篇博客对想要了解重载的人有所帮助吧。
四、参考
- 《深入理解Java虚拟机》