面向对象的三个特性
面向对象有三个重要的特性:多态,继承,封装。
多态的表现
多态的在java中的应用体现在方法的重载和重写。
重载:字面上的意义一个类里面,有同名但是参数必须不相同的方法。(这里的”不同参数“必须不是泛型,比如List<String> 和List<Integer>)
重写:对应的是子类去重写了父类的方法,有自己的实现,不能更改方法的参数和名称。
但是在JVM中是怎么实现的呢?JVM对于这两种方式是怎么区分的?
方法的重载
我们都知道,.java的源文件都会被编译成.class的文件,class文件是一堆二进制流。
执行 方法的指令在JVM里面有四个
invokestatic 执行静态的方法
invokespecial 执行构造方法,或者调用父类的方法(对应super关键字)
invokedynamic 执行动态语言的方法
invokevirtual 执行普通方法
考虑下面代码
class A{
pulic void print(B b){
}
pulic void print(C c){
}
}
public static void main(String args[]){
A a = new A();
B b = new C();
a.print(b);
}
当我们调用 a.print(b)的时候,虚拟机会根据你传递参数的静态类型去匹配方法,这是一个静态分派的过程。
什么叫静态类型 比如 B b = new C();C是派生B的,我们经常在java中这么写,没有任何问题。这里对象B的静态类型就是B,但是它的实际类型是C。
我们调用a.print(b)的时候它匹配的是 print(B b)这个方法,所以编译器它不会考虑这个b的实际类型(因为B的实际类型会被执行invospecial去指定)是什么,它直接在编译成字节码阶段就把这个方法匹配了。
它的字节码
但是还有一个点要注意,静态类型是可以强转的
public static void main(String args[]){
A a = new A();
B b = new C();
a.print((C)b);
}
这时候它匹配的就是print(C c)这个方法了
方法重载:只会根据参数的静态类型去匹配。
方法的重写
方法的重写:表现的就是重写父类的方法
public class B {
public void callMe(){
System.out.print("B is call");
}
}
public class C extends B {
@Override
public void callMe() {
System.out.print("C is call");
}
}
public static void main(String args[]){
B b = new B();
b.callMe();
B c = new C();
c.callMe();
}
在编译阶段它只会在静态类型中去匹配方法,所以尽管b和c 的实际类型不同,但它们执行callMe的字节码指令是相同的
但是虚拟机在执行这个指令的时候,它会先在操作数栈中拿到调用这个方法的对象,然后根据这个对象的实际类型去匹配方法,如果这个类型里面没有,则它就会去父类去找一直递归直到找到为止。否则将会抛出MethodNotFoundException。
这样递归非常麻烦,所以虚拟机会对每一个clas对象都生成一个方法表。(这个在连接阶段会准备好,一般再类变量初始为零值之后)
它里面保存了这个类所有方法直接引用(也是就能再虚拟机中找到执行的代码),保存的顺序先是父类的方法,然后才是自己的方法,如果子类重写了父类的方法,它会将这个指针指向自己的实现,否则指向父类,这样的好处是只用查找一次,不管类型怎么切换,只要他们的方法表的顺序是一致的,我就可以根据这个下标去找到对应的方法入口。
比如使用son的对象 son.hardChoice(),会再son的方法表里面找到方法入口,虚拟机会记住这个下标,当我再使用
father.hardChoice()的时候,直接切换到Father的方法表,通过记住的这个下标找到方法入口,不用去遍历了!
同理接口也有对应的接口表,它里面的方法就是无序的了,无法通过下标去查找,每一次调用方法都只能去遍历整个表,所以一般来说查找接口方法比查找普通方法要慢。