该例子参考了书《深入理解Java虚拟机》。

首先,思考下面代码的返回结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class StaticDisptach{

static abstract class Human{}

static class Man extends Human{}

static class Woman extends Human{}

public void sayHello(Human people){
System.out.println("hello,people");
}

public void sayHello(Man man){
System.out.println("hello,man");
}

public void sayHello(Woman woman){
System.out.println("hello,woman");
}

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

}

运行结果是:

hello,people

hello,people

如果对重载有所了解,得到这个结果并不难。但是为什么虚拟机会执行参数类型为Human的重载版本呢?

首先我们定义两个关键概念,对于如下代码:

1
Human man = new Man(); 

上面代码的Human 称为变量的“静态类型”,或者“外观类型”,后面的Man被称为变量的“实际类型”或者“运行时类型”。

静态类型和实际类型在程序中都可能会发生变化,区别是静态类型变化仅仅在使用时才会发生,变量本身静态类型不会改变,并且最终静态类型在编译期可知;实际类型变化的结果只有在运行期才可知,编译期在编译阶段并不知道对象的实际类型是什么。

这句话什么意思呢?

1
2
3
4
Human human = (new Random()).nextBoolean()?new Man():new Woman;

sr.sayHello((Man) human);
sr.sayHello((Woman) human);

对象human的实际类型是可变的,编译期你不可能知道它究竟是Man还是Woman,需要等到运行期才知道。而human的静态类型Human可以在使用时通过强制类型转换临时改变,但这个改变在编译期可知——两次调用sayhello方法,编译期可以知道转型的是Man还是Woman

因此,对于之前的重载,对于sayHello方法,方法接收者已经确定是对象“sr”的前提下,使用哪个重载版本就完全取决于传入参数的数量和数据类型。编译期在重载时是通过参数的静态类型而不是实际类型做判断的。由于静态类型在编译期可知,所以在编译阶段,Javac编译期就根据参数的静态类型决定了会使用哪个重载版本,因此选择了sayHello(Human)作为调用目标,并把这个方法的符号引用写到main()方法里面的两条invokevirtual指令的参数中。

另外,Javac编译期虽然能确定方法的重载版本,但是这个重载版本并不是唯一的,往往只能确定一个更合适的版本。因此有了重载方法匹配优先级的说法:(这种情况的出现主要是因为字面量天生的语义模糊)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class A {
public static void sayHi(Object arg){
System.out.println("object");
}

public static void sayHi(int arg){
System.out.println("int");
}

public static void sayHi(long arg){
System.out.println("long");
}

public static void sayHi(Character arg){
System.out.println("Character");
}

public static void sayHi(char arg){
System.out.println("char");
}

public static void sayHi(char... arg){
System.out.println("char...");
}

public static void sayHi(Serializable arg){
System.out.println("Serializable");
}

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

}

上述代码运行结束后会生成 “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.Serializablejava.lang.Character 类所实现的一个接口,当自动装箱还是找不到装箱类,但是找到了装箱类所实现的接口类型,所以会再做一次自动装箱。

char可以转型为 int,但是Character不会转型为Integer的,只能安全地转型为它实现的接口或者父类。

这里有个小细节, Character 还实现了另一个接口 Comparable<Character> ,如果有两个参数分别为 SerializableComparable<Character>,它们此时的优先级是一样的,编译器会提示类型模糊并拒绝编译。此时需要显式调用。

如果继续注释掉 sayHi(Serializable arg)方法,结果就会变成:

Object

很明显这是char装箱后转型为父类了,如果有多个父类会从下往上搜索,Object是优先级最低的。

最后,把 sayHi(Object arg) 方法注释掉,还有一个输出结果:

char…

可见变长参数的优先级是最低的,这时候字符’a’被当做了一个char[] 数组的元素。

以上例子属于比较极端的例子,除了面试时用作难为求职者外实际工作中很难有所用途,但是有所了解相信也对java的进一步认知有帮助。