面向对象基本特征:多态

多态是面向对象最重要的特征。具体到Java中是如何体现的呢。

多态在我们的使用中其实就是重载与重写。下面分别进行讲述一下。

重载

重载的定义:一个类中,如果有两个方法的方法名相同,但参数列表不同,可以说一个方法是另一个方法的重载。

注意2点:1.方法名相同   2.参数列表不同(参数列表为:参数的类型,参数的个数)

调用重载方法的时候,JVM会根据不同的参数列表来选择合适的方法。

我们来看一下代码:

public class Main {
    public static void main(String[] args) {
        Father f= new Father();
        f.print();
        f.print(5);
    }
}
class Father{public void print() {
        System.out.println("father");
    }public void print(int x) {
        System.out.println(x);
    }
}

看一下字节码:

可以很明显看到invokevirtual调用了不同的方法。

重写

当子类拥有和父类相同的方法(方法名,参数列表相同),则说子类重写了父类的方法。

注:1.方法名相同  2.参数列表相同  3  子类重写方法的访问修饰符必须大于等于父类的方法。(比如父类为 protected void print(){},则子类必须protected或者public)

来看一下重写的例子:

public class Main {
    public static void main(String[] args) {
        Father son = new Son();
        son.print();
    }
}
class Father{public void print() {
        System.out.println("father");
    }
}
class Son extends Father{public void print() {
        System.out.println("son");
    }
}

结果很明显:son

看一下字节码:

从第9行可以看到:invokevirtual指令的注释显示了是Father.print()的符号引用。但是为什么结果是son而不是father呢。

首先要说关于静态类型和实际类型:

Father son = new Son(); 中

Father称为静态类型,而Son称为实际类型。区别在于静态类型在编译期就会确定,而实际类型要在运行期才能确定。编译器编译程序时候并不知道对象的实际类型是什么。

所以由于重载在编译期就确定了类型。所以这个过程也可以叫静态分派。而重写是动态分派。

来看一下invokevirtual指令的解析过程(参考《深入理解Java虚拟机》):

1.找到操作数栈顶的第一个元素所指向的对象的实际类型。记作C(在上面代码实际类型为Son)

 2.如果在类型C中找到与常量中描述符合简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用。查找过程结束。不通过的话则返回java.lang.IllegalAccessError异常

(上面代码指的就是查找print()方法,在Son中找到了。所以就成功结束查找过程)

3.否则,按照继承关系从下往上一次对C的各个父类进行第2步的搜索和验证过程。

4.如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。

由于invokevirtual指令执行的第一步就是在运行期确定实际类型。所以invokevirtual指令把常量池的方法的类方法符号引用(即print())解析到了Son的直接引用中。所以我们具体看到的结果就是Son.print()。

这就是重写的本质。

 

关于Father f  = new Son() 中 f的方法问题。

在题目中经常会看到关于下面的题目:

public class Main {
    public static void main(String[] args) {
        Father son = new Son();
        son.print();
    }
}
class Father{
public void print() { System.out.println("father"); } } class Son extends Father{
public void printSon() { System.out.println("son"); } }

结果大家都能记住:father

这里就可以用invokevirtual指令的解析顺序来解释:C还是Son类,但是Son中并没有print()方法,所以往父类找。在父类找到了返回print()方法的直接引用。所以最后调用的是Father.print()。

注:以前看视频这个是父类引用指向子类的实例对象。向上转型之类的,列举一堆例子概念让人记住结果而已。反正我现在已经不怎么记得了。

当从invokevirtual指令解析顺序去理解,其实整个过程会变得清晰多了。

posted @ 2019-02-09 23:22  发包哥哥  阅读(327)  评论(0编辑  收藏  举报