从字节码角度分析重载与重写
代码分析
/**
方法的静态分派
Grandpa g1 = new Father();
以上的代码,g1的静态类型是Grandpa,而g1得实际类型(真正指向的类型)是Father.
我们可以得出这样一个结论:变量的静态类型是不会发生变化的,而变量的实际类型则是可以发生变化的
(多态的一种体现),实际类型实在运行期可确定
*/
public class Test5 {
//方法重载,是一种静态的行为,这种静态行为,在编译期可以完全确定
//方法重写,则是一种动态的行为
public void test(Grandpa grandpa) {
System.out.println("grandpa");
}
public void test(Father father) {
System.out.println("father");
}
public void test(Son son) {
System.out.println("son");
}
public static void main(String[] args) {
Grandpa g1 = new Father();
Grandpa g2 = new Son();
Test5 test5 = new Test5();
/**
* 这里我们是不是一开始以为会输出father与son?
* 但是实际输出的grandpa与grandpa
* 如何从理论推导?
*/
//26 invokevirtual #13 <com/chen/jvm/bytecode/Test5.test>
test5.test(g1); //grandpa
test5.test(g2); //grandpa
}
}
class Grandpa {
}
class Father extends Grandpa {
}
class Son extends Father {
}
public class Test6 {
public static void main(String[] args) {
Fruit apple = new Apple();
Fruit oranage = new Oranage();
//17 invokevirtual #6 <com/chen/jvm/bytecode/Fruit.test>
/**
从字节码中我们可以看出,其在编译器指向的是Fruit.text
方法的动态分派
方法的动态分派涉及到一个重要概念:方法接收者
invokevirtual字节码指令的多态查找流程
1.找到操作数栈顶去寻找到栈顶的元素对象所指向的实际类型
2.如果在实际类型的常量池找到是否有对应的方法以及权限通过,如果符号就返回该对象的引用
3.如果没有找到,就沿着这个流程找其父类
比较方法重载与方法重写,我们可以得到结论
方法重载是静态的,是编译器行为;方法重写是动态的,是运行期行为
*/
apple.test();//Apple
oranage.test();//Oranage
apple = new Oranage();
apple.test();//Oranage
}
}
class Fruit {
public void test() {
System.out.println("Fruit");
}
}
class Apple extends Fruit {
//多态,是一种动态的行为
@Override
public void test() {
System.out.println("Apple");
}
}
class Oranage extends Fruit {
@Override
public void test() {
System.out.println("Oranage");
}
}
字节码分析
符号引用,直接引用
有些符号引用是在类加载阶段或是第一次使用时就会转换为直接引用,这种转换叫做静态解析;
另外一些符号引用则是在每次运行期直接转换为直接引用,这种转换叫做动态链接,这体现为Java的多态性
相关字节码说明
1.invokeinterface:调用接口中的方法,实际上是在运行期决定的,决定到底调用实现该接口的那个对象的特定方法
2.invokestatic:调用静态方法
3.invokespecial:调用自己的私有方法、构造方法(<init>)以及父类的方法
4.invokevirtual:调用虚方法,运行期动态查找的过程(java多态的基础)。其实只有调用虚方法的时候,是没有唯一确定版本的,也就是可以产生多态的,上面的那些是唯一确定了版本的。
5.invokedynamic:动态调用方法(jdk1.7引进的)
静态解析(字节码还没有运行的时候,就可以解析,就可以确定调用那个对象的方法)的四种情形:
1.静态方法
2.父类方法
3.构造方法
4.私有方法(因为公有方法是有可能被重写的)
以上4类方法称为非虚方法,他们是在类加载阶段就可以将符号引用转换为直接引用的。
虚方法:除了以上的非虚方法+final修饰的方法外其他的方法都称为虚方法。
Test5.class的Main方法的Code
public static void main(String[] args) {
Grandpa g1 = new Father();
Grandpa g2 = new Son();
Test5 test5 = new Test5();
test5.test(g1);
test5.test(g2);
}
0 new #7 <com/chen/jvm/bytecode/Father>
3 dup
//调用自己的私有方法,构造方法
4 invokespecial #8 <com/chen/jvm/bytecode/Father.<init>>
7 astore_1
8 new #9 <com/chen/jvm/bytecode/Son>
11 dup
12 invokespecial #10 <com/chen/jvm/bytecode/Son.<init>>
15 astore_2
16 new #11 <com/chen/jvm/bytecode/Test5>
19 dup
20 invokespecial #12 <com/chen/jvm/bytecode/Test5.<init>>
23 astore_3
24 aload_3
25 aload_1
26 invokevirtual #13 <com/chen/jvm/bytecode/Test5.test>
29 aload_3
30 aload_2
31 invokevirtual #13 <com/chen/jvm/bytecode/Test5.test>
34 return
Test6.class的Main方法的Code
3 dup
4 invokespecial #3 <com/chen/jvm/bytecode/Apple.<init>>
7 astore_1
8 new #4 <com/chen/jvm/bytecode/Oranage>
11 dup
12 invokespecial #5 <com/chen/jvm/bytecode/Oranage.<init>>
15 astore_2
16 aload_1
//这里也是同上面一样的
17 invokevirtual #6 <com/chen/jvm/bytecode/Fruit.test>
20 aload_2
21 invokevirtual #6 <com/chen/jvm/bytecode/Fruit.test>
24 new #4 <com/chen/jvm/bytecode/Oranage>
27 dup
28 invokespecial #5 <com/chen/jvm/bytecode/Oranage.<init>>
31 astore_1
32 aload_1
33 invokevirtual #6 <com/chen/jvm/bytecode/Fruit.test>
36 return
我们从字节码分析,方法的重载与重写其底层的字节码都是invokevirtual:调用虚方法,运行期动态查找的过程。那么为什么结果不一样呢?
/**
从字节码中我们可以看出,其在编译器指向的是Fruit.test
方法的动态分派
方法的动态分派涉及到一个重要概念:方法接收者。这一点很重要。
invokevirtual字节码指令的多态查找流程
1.找到操作数栈顶去寻找到栈顶的元素对象所指向的实际类型
2.如果在实际类型的常量池找到是否有对应的方法以及权限通过,如果符号就返回该对象的引用
3.如果没有找到,就沿着这个流程找其父类
比较方法重载与方法重写,我们可以得到结论
方法重载是静态的,是编译器行为;方法重写是动态的,是运行期行为
*/
//方法重载
Test5 test5 = new Test5();
test5.test(g1); //其方法的接收者是Test5,
test5.test(g2);
//方法重写中
apple.test();//其方法的接收者是Fruit,会找到操作数栈顶去寻找到栈顶的元素对象所指向的实际类型。所以真正调用Apple中的test方法
oranage.test();
验证
/**
针对于方法调用动态分配的过程,虚拟机会在类的方法区建立一个虚方法表的数据结构(virtual method table)
针对于invokeinterface指令来说,虚拟机会建立一个叫做接口方法表的数据结构
*/
public class Test7 {
public static void main(String[] args) {
Animal animal = new Animal();
Animal dog = new Dog();
animal.test("hhh");//animal str
dog.test(new Date());//dog date
//下面这行代码在编译期就不能通过,原因是为什么呢?
//因为在字节码对应的是一个invokevirtual,会编译成<com/chen/jvm/bytecode.Animal.test2>,
// 而Animal中并没有test2这个方法 ,所以编译也是通不过的。
// dog.test2();
}
}
class Animal {
public void test(String str) {
System.out.println("animal str");
}
public void test(Date date) {
System.out.println("animal date");
}
}
class Dog extends Animal {
@Override
public void test(String str) {
System.out.println("dog str");
}
@Override
public void test(Date date) {
System.out.println("dog date");
}
public void test2() {
}
}