java虚拟机基础知识大全 java虚拟机运行原理( 八 )


sd.((Woman)man);
所有依赖静态类型来决定方法执行版本的分派动作,都称为静态分派 。静态分派的最典型应用表现就是方法重载 。静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的,这点也是为何一些资料选择把它归入“解析”而不是“分派”的原因 。
class{
void (int i){
.out.("int 类型");
}
void ( obj){
.out.("obj 类型");
}
void (long i){
.out.("long 类型");
}
void (char i){
.out.("char 类型");
}
void main([] args) {
(1);
(1L);
('a');
}
}
笔者讲述的解析与分派这两者之间的关系并不是二选一的排他关系,它们是不同层次上去筛选、确定目标方法的过程 。例如,前面说过,静态方法会在类加载期就进行解析,而静态方法显然也是可以拥有重载版本的,选择重载版本的过程也是通过静态分派完成的 。
自动转型还能继续发生多次,按照char>int>long>float>的顺序转型进行匹配,但不会匹配到byte和short类型的重载,因为char到byte或short的转型是不安全的 。
自动装箱
装箱后转型为父类了,如果有多个父类,那将在继承关系中从下往上开始搜索,越接上层的优先级越低 。
可见变长参数的重载优先级是最低的,这时候字符'a'被当作了一个char[]数组的元素 。
有一些在单个参数中能成立的自动转型,如char转型为int,在变长参数中是不成立的
Java语言多态性的另外一个重要体现——重写() 。
class{
class Human{
void ();
}
class ManHuman{
@
void () {
.out.("man say hello!");
}
}
class WomanHuman{
@
void () {
.out.("woman say hello!");
}
}
void main([] args) {
Human man=new Man();
Human woman=new Woman();
man.();
woman.();
man=new Woman();
man.();
}
}
输出结果:
man say hello!
woman say hello!
woman say hello!
根据《Java虚拟机规范》,指令的运行时解析过程[插图]大致分为以下几步:
1)找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C 。
2)如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;不通过则返回java.lang.异常 。
3)否则,按照继承关系从下往上依次对C的各个父类进行第二步的搜索和验证过程 。
4)如果始终没有找到合适的方法,则抛出java.lang.异常 。
正是因为指令执行的第一步就是在运行期确定接收者的实际类型,所以两次调用中的指令并不是把常量池中方法的符号引用解析到直接引用上就结束了,还会根据方法接收者的实际类型来选择方法版本,这个过程就是Java语言中方法重写的本质 。我们把这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派 。
既然这种多态性的根源在于虚方法调用指令的执行逻辑,那自然我们得出的结论就只会对方法有效,对字段是无效的,因为字段不使用这条指令 。事实上,在Java里面只有虚方法存在,字段永远不可能是虚的,换句话说,字段永远不参与多态,哪个类的方法访问某个名字的字段时,该名字指的就是这个类能看到的那个字段 。当子类声明了与父类同名的字段时,虽然在子类的内存中两个字段都会存在,但是子类的字段会遮蔽父类的同名字段 。
输出两句都是“I am Son”,这是因为Son类在创建的时候,首先隐式调用了的构造函数,而构造函数中对()的调用是一次虚方法调用,实际执行的版本是Son::()方法,所以输出的是“I am Son”,这点经过前面的分析相信读者是没有疑问的了 。而这时候虽然父类的money字段已经被初始化成2了,但Son::()方法中访问的却是子类的money字段,这时候结果自然还是0,因为它要到子类的构造函数执行时才会被初始化 。main()的最后一句通过静态类型访问到了父类中的money,输出了2 。