方法静态分派
该例子参考了书《深入理解Java虚拟机》。
首先,思考下面代码的返回结果:
1 |
|
运行结果是:
hello,people
hello,people
如果对重载有所了解,得到这个结果并不难。但是为什么虚拟机会执行参数类型为Human的重载版本呢?
首先我们定义两个关键概念,对于如下代码:
1 |
|
上面代码的Human
称为变量的“静态类型”,或者“外观类型”,后面的Man
被称为变量的“实际类型”或者“运行时类型”。
静态类型和实际类型在程序中都可能会发生变化,区别是静态类型变化仅仅在使用时才会发生,变量本身静态类型不会改变,并且最终静态类型在编译期可知;实际类型变化的结果只有在运行期才可知,编译期在编译阶段并不知道对象的实际类型是什么。
这句话什么意思呢?
1 |
|
对象human
的实际类型是可变的,编译期你不可能知道它究竟是Man
还是Woman
,需要等到运行期才知道。而human
的静态类型Human
可以在使用时通过强制类型转换临时改变,但这个改变在编译期可知——两次调用sayhello
方法,编译期可以知道转型的是Man
还是Woman
。
因此,对于之前的重载,对于sayHello
方法,方法接收者已经确定是对象“sr”的前提下,使用哪个重载版本就完全取决于传入参数的数量和数据类型。编译期在重载时是通过参数的静态类型而不是实际类型做判断的。由于静态类型在编译期可知,所以在编译阶段,Javac编译期就根据参数的静态类型决定了会使用哪个重载版本,因此选择了sayHello(Human)
作为调用目标,并把这个方法的符号引用写到main()
方法里面的两条invokevirtual
指令的参数中。
另外,Javac编译期虽然能确定方法的重载版本,但是这个重载版本并不是唯一的,往往只能确定一个更合适的版本。因此有了重载方法匹配优先级的说法:(这种情况的出现主要是因为字面量天生的语义模糊)
1 |
|
上述代码运行结束后会生成 “char
”
这很好理解,因为'a'
就是一个char类型数据。但是如果把sayHi(char arg)
方法注释掉,输出会变成:
int
这是因为发生了自动类型转换,'a'
不仅代表一个字符,还代表数字97(Unicode)。现在再把 sayHi(int arg)
注释掉,结果就变成了:
long
这是因为发生了两次自动类型转换,'a'
从字符转型成整数97,然后转型为长整数 97L,自动类型转换可能还可以继续进行,即
char -> int -> long -> float -> double的顺序
注意没有short和byte的转型,因为它们的转型是不安全的。
接下来如果继续注释掉 sayHi(long arg)
方法,结果就会变成:
Character
这是因为发生了自动装箱,如果继续注释 sayHi(Character arg)
,则输出会变为:
Serializable
之所以会出现 Serializable 这样的结果,是因为 java.lang.Serializable
是 java.lang.Character
类所实现的一个接口,当自动装箱还是找不到装箱类,但是找到了装箱类所实现的接口类型,所以会再做一次自动装箱。
char可以转型为 int,但是Character
不会转型为Integer
的,只能安全地转型为它实现的接口或者父类。
这里有个小细节, Character
还实现了另一个接口 Comparable<Character>
,如果有两个参数分别为 Serializable
和 Comparable<Character>
,它们此时的优先级是一样的,编译器会提示类型模糊并拒绝编译。此时需要显式调用。
如果继续注释掉 sayHi(Serializable arg)
方法,结果就会变成:
Object
很明显这是char装箱后转型为父类了,如果有多个父类会从下往上搜索,Object是优先级最低的。
最后,把 sayHi(Object arg)
方法注释掉,还有一个输出结果:
char…
可见变长参数的优先级是最低的,这时候字符’a’被当做了一个char[] 数组的元素。
以上例子属于比较极端的例子,除了面试时用作难为求职者外实际工作中很难有所用途,但是有所了解相信也对java的进一步认知有帮助。