在聊静态分派前,我们先看一段代码,想象一下程序的输出结果是什么。后面我们将围绕这个类的方法来重载(overload)代码,以分析虚拟机和编译器确定方法版本的过程。代码如下:
运行结果:
hello gay
hello gay
以上代码实际上是考察我们对重载的理解程度,相信对java变成稍有经验的程序员看完程序后都能得出正确的运行结果,但为什么会选择执行参数为Human的重载呢?在解决这个问题之前我们先按如下代码定义两个重要的概念。
Human man = new Man();
我们把上面的Human成为变量的静态类型(Static Type),或者叫做外观类型(Apparent Type),后面的Man则称为变量的实际类型(Actual Type),静态类型和实际类型在程序中都可以发生一些变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型实在编译期可知的;而实际类型变化的结果在运行期才能确定,编译期在编译程序的时候并不知道一个对象的实际类型是什么。
代码中main()里面的两次sayHello()方法调用,在方法接受者已经确定是对象sd的前提下,使用哪个版本重载版本,就完全取决于传入参数的数量和数据类型。代码中刻意的定义了两个静态类型相同但实际类型不同的变量,但虚拟机(准确的说编译器)在重载时是通过参数的静态类型而不是实际类型作为判定依据。并且静态类型是编译期可知的,因此,在编译阶段,Javac编译器会根据参数的静态类型决定使用哪个重载版本,所以选择sayHello(Human)作为调用目标,并把这个方法的符号引用写到main()方法里的两条invokevirtual指令参数中。
所有依赖静态类型来定位方法执行版本的分派动作成为静态分派。静态分派的典型应用是方法重载。静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。另外,编译期虽然能确定出方法的重载版本,但在很多情况下这个重载版本并不是“唯一的”,往往只能确定一个“更加合适的”版本。这种模糊的结论在由0和1构成的计算机世界中算是比较稀罕的事情,产生这种模糊结论的主要原因是字面量不需要定义,所以字面量没有显示的静态类型,他的静态类型只能通过语言上的规则去理解和推断。如下代码演示了何为“更加合适的”版本。
上面的代码运行后输出:
hello char
这很好理解,’a‘是一个char类型的数据,自然会找到参数类型为char的重载方法,如果注释掉sayHello(char arg)方法,那么输出会变为:
hello int
这是发生了一次自动类型转换,’a‘除了可以代表一个字符串,还可以代表输在97,因此参数类型为int的重载也是合适的。没门继续注释掉sayHello(int arg)方法,那么输出会变为:
hello long
这是发生了两次自动类型转换,’a‘转换类型为97之后,进一步转换类型为长整数97L,匹配了参数为long的重载,以上代码没有写其他类型如float,double等的重载,不过实际上自动转型还能继续发生多次,按照char>int>long>float>double的顺序转型进行匹配。但不会匹配byte和short类型的重载,因为char到byte或short的转型是不安全的。我们继续注释掉sayHello(long arg)方法,那么会输出:
hello Character
这时发生了一次自动装箱,’a‘被包装他的封装类型java.lang.Character,所以匹配到了参数类型为Character的重载,继续注释掉sayHello(Character arg)方法,那么输出会变为:
hello Serializable
这个输入可能会让人疑惑,一个字符或数字与序列化有什么关系?出现hello Serializable ,是因为java.lang.Serializable 是java.lang.Character类实现的一个接口类型,所以紧接着又发生了一次自动转型。char可以转换成int,但是Character是绝对不会转型为Integer的,他只能安全的转型为他实现的接口或父类。Character还实现了另外一个接口java.lang.Comparable,如果同时出现两个参数分别为Serializable和Comparable的重载方法,那他们在此时的优先级是一样的。编译期无法确定要自动转型为哪种类型,会提示类型模糊,拒绝编译。程序必须在调用时显示的指定字面量的静态类型,如:sayHello((Serializable)'a'),才能编译沟通过。下面继续注释掉sayHello(Serializable arg),输出将会变为:
hello object
这时是char装箱后转型为父类型,如果有多个父类,那将在继承关系中从下往上开始搜索,越接近上层的优先级越低。即使方法调用传入的参数值为null时,这个规则仍然适用。我们把sayHello(Object arg)也注释掉,输入将会变为
sayHello char...
7个重载方法已经注释掉只剩一个了,可见变长参数的重载优先级是最低的,这时候字符’a‘被当做了一个数组元素。但要注意的是,有一些在单个参数中能成立的自动转型,如char转型int,在变长参数中是不成立的。