12-面向对象5(多态)
Posted liujiaqi1101
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了12-面向对象5(多态)相关的知识,希望对你有一定的参考价值。
看了视频,讲师就只说“编译看左边,运行看右边”,讲的跟玄学似的;我可不能那么肤浅!于是看了很多博客,现摘下来做个整合,整我一头汗,图书馆还不开空调,热死我了
JVM
-
Java源代码被编译器编译成class文件(不是底层操作系统可以直接执行的二进制指令)。因此,我们需要一种平台可以解释class文件并运行它。而做到这一点的正是JVM
-
实际上,JVM是一种解释执行class文件的规范技术,各个提供商都可以根据规范,在不同的底层平台上实现不同的JVM
-
JVM实现的基本结构图
-
运行时数据区
当JVM运行一个程序时,需要在内存存储许多东西。比如字节码、程序创建的对象、传递的方法参数、返回值、局部变量等等。JVM会把这些东西都组织到几个"运行时数据区"中便于管理
- 【栈】栈中的数据是线程私有的,一个线程是无法访问另一个线程的栈的数据
- 【方法区】
- 【堆】
多态引入
- 【抽象理解】同一个对象,不同时刻表现出来的不同状态
- 继承允许将对象视为他自己本身的类型或其基类型来加以处理。既然允许将多种类型(从同一基类导出的)视为同一类型来处理,那么同一份代码也就可以毫无差别的运行在这些不同类型之上了
- 多态机制使具有不同内部结构的对象可以共享相同的外部接口
- 作用:消除类型之间的耦合关系
- 体现:父类引用指向子类对象
- 使用前提
- 要有继承关系
- 要有方法重写
- 父类引用指向子类对象
对象的转型
- 向上转型upcasting
- 由于对象既可以当作它自己本身的类型使用,也可以当作它的基类类型使用,编译器是允许的;这个转换可以自动进行
- 因此将某个对象的引用是其基类类型的引用的行为称为"向上转型",通俗来讲就是"父类引用指向子类对象"
- 向下转型downcasting(造型)
- 如果父类引用的对象是父类本身,那么在向下转型的过程中是不安全的,必须进行造型(强制类型转换),才能够通过编译时的检查 // 强制类型转换要求双目必须有子父类关系,否则编译报错
- 错误的类型转换,就算躲过编译器检查,运行时也会引发
java.lang.ClassCastException
;可以使用instanceof
来测试一个对象的类型
- obj instanceof T
- obj 为一个对象,T 表示一个类或者一个接口,当 obj 为 T 的对象,或者是其直接或间接子类,或者是其接口的实现类,结果result 都返回 true,否则返回false
- 无继承关系的引用类型间的转换是非法的!所以,obj 所属的类与类T必须是子类和父类的关系,否则instanceof编译报错!
对象的类型
- 编译时类型
- 编译时类型由声明该变量时使用的类型决定
- 一个引用类型变量如果声明为父类的类型,但实际引用的是子类对象,那么该变量就不能再访问在子类中添加的属性和方法
- 属性也是在编译时确定的,编译时引用声明为Person类型,没有Son特有成员变量,因而编译错误
- 方法同理,只能调用父类中的方法,不能调用子类特有方法,否则也会编译报错
- 运行时类型
- 运行时类型由实际赋给该变量的对象决定
- 若编译时类型和运行时类型不一致,就出现了对象的多态性
绑定
将 [一个方法调用] 与 [一个方法主体] 关联起来,被称为"绑定"
静态绑定
若这个绑定时发生在程序执行之前(如:由编译器或链接程序实现的),则称为"前期绑定"/"静态绑定"
- 静态绑定过程
- 如果一个方法有static、private、final修饰或者是构造方法,那就都是"前期绑定"
- 所有的静态方法都是"前期绑定",因为静态方法可以通过类名进行访问,而不会用到引用的对象的实际类型信息,因此在编译时就可以通过类型信息确定是哪一个具体的方法
- 这也就揭示了为什么静态方法不能重写了,因为重写的目的是为了实现多态
- private方法 默认是 final类型
- 构造方法其实是一种特殊的 static 方法
- 成员变量也属于"前期绑定"
- 调用到的成员变量为父类的属性!
- 也就是说运行时(动态)绑定针对的范畴只是 [对象的方法]
- 总结调用一个方法的过程(摘自Java核心技术卷1)
- 编译器查看对象的声明类型和方法名。假设调用x.f(param),且隐式参数 x 声明为 Father 类的对象。需要注意的是:有可能存在多个名字为 f,但参数类型不一样的方法。例如,可能存在方法f(int)和方法f(String)。编译器将会一一列举所有Father类中名为f的方法和其超类中访问属性为 public 且名为 f 的方法(超类的私有方法不可访问)。至此,编译器已获得所有可能被调用的候选方法
- 接下来,编译器将查看调用方法时提供的参数类型。如果在所有名为 f 的方法中存在一个与提供类型完全匹配,就选择这个方法。这个过程被称为重载解析。例如:对于调用x.f("Hello")来说,编译器将挑选出f(String),而不是f(int)。由于允许类型转换(int可以转换成double,Manager可以转换成Employee,Circle可以转换成Object,等等)所以这个过程可能很复杂。如果编译器没有找到与参数类型匹配的方法,或者发现经过类型转换后有多个方法与之匹配,就会报告一个错误。至此,编译器已获得需要调用的方法名字和参数类型。
- Java中 [重载方法] 的选择是"静态绑定"
- 也就是一个方法的参数选择是静态绑定的
- 如调用了一个重载的方法,在 编译时 根据参数列表就可以确定方法,并且如果这个方法是非静态的,那么具体调用的是父类的方法还是子类的方法还需要通过"动态绑定"来确定
- 当程序运行时,并且采用"动态绑定"调用方法时,JVM一定会调用与x所引用对象的实际类型最合适的那个类的方法
动态绑定
这个绑定发生在程序运行之中,根据对象的具体类型进行绑定的,那么这种绑定称为"动态绑定"
- 动态绑定是基于对象的实际类型而非对象的引用类型!(摘自Java编程思想)
- 再提【方法区】
- 每次调用方法都要进行搜索,时间开销相当。因此会在JVM加载类的同时,在方法区中会为每个类存放很多信息。而存放的类的信息中有一个数据结构叫【方法表】。它以 {数组} 的形式记录了当前类及其所有超类的可见方法字节码在内存中的直接地址。这样一来,在真正调用方法的时候,JVM仅查找这个表就行了
- 子类方法表中继承了父类的方法
- 相同的方法(相同的方法签名:方法名和参数列表相同。返回值不是签名的一部分,但在覆盖时要保证返回类型的兼容性,即:允许子类将覆盖方法的返回类型定义为原返回类型的子类型)在所有类的方法表中的索引相同
- 如果调用 "super.f()",编译器将对隐式参数超类的方法表进行搜索
- 动态绑定过程
- 【常量池解析】根据 [测试类常量池中第15个常量表] 中记录的 "方法f1信息的符号引用" 来查找引用father所在对应类Father,继而找到该类对应的方法表,然后将找到的Father方法表中 "f1方法对应的索引项11" 再放置回 [测试类的常量池中的第15个常量表] 中 ,这个过程是用 Father方法表中的索引项 来代替 常量池中的符号引用
- 在编译阶段,最佳方法名依赖于这个父类引用;而决定方法是哪个类的版本,这通过由JVM推断出这个对象的运行时类型来完成
- 换言之,在编译阶段,该被调用的方法就已经确定好了(如上:父类方法表第11个方法)。而具体调用谁的方法实现,这还要看具体引用所指向的对象是谁 → 该对象所属类的方法表中 同样是第11个方法数据的指针 所指向的方法字节码所在的存储空间,那里存放的才是真正要被执行的方法体
- 体现在代码上,就是:能够被调用的只有父类中声明的方法,子类特有方法不能通过父类引用来调用
- 对于如下的调用方法,JVM是如何定位的呢?
- 问题就在于Father类型中并没有方法签名为f1(char)的方法呀。但打印结果显示JVM调用了Father类型中的f1(int)方法,并没有调用Son类型中的f1(char)方法
- 根据上面详细阐述的调用过程,首先可以明确的是:JVM首先是根据对象father声明的类型Father来解析常量池的(解析:用Father方法表中的索引项来代替常量池中的符号引用)。如果Father中没有匹配到"合适"的方法,就无法进行常量池解析,这程序在编译阶段就通过不了。那既然通过了,就说明还是"合适"~
- 那什么叫"合适"的方法呢?
- 方法签名完全一样的方法自然是合适的。但如果方法中的参数类型在声明的类型中并不能找到呢?比如上面的代码调用father.f1(),Father类型并没有f1(char)的方法签名
- 实际上,JVM会找到一种"凑合"的办法,就是通过 [参数的自动转型] 来找到"合适"的办法。比如char可以通过自动转型成int,那么Father类就可以匹配到这个方法了
小结:overload / overwrite
code
向上转型 → 编译时类型 & 运行时类型 → 动态绑定
class Animal {
protected void eat() {
System.out.println("animal eat food");
}
}
class Cat extends Animal {
protected void eat() {
System.out.println("cat eat fish");
}
}
class Dog extends Animal {
public void eat() {
System.out.println("Dog eat bone");
}
}
class Sheep extends Animal {
public void eat() {
System.out.println("Sheep eat grass");
}
}
// 多态是编译时行为还是运行时行为?
public class Test {
public static Animal getInstance(int key) {
switch (key) {
case 0:
return new Cat ();
case 1:
return new Dog ();
default:
return new Sheep ();
}
}
public static void main(String[] args) {
int key = new Random().nextInt(3);
System.out.println(key);
Animal animal = getInstance(key);
animal.eat();
}
}
public class Test2 {
public static void main(String[] args) {
Base base = new Sub();
base.add(1, 2, 3); // "sub_1"; 可变参数和数组不构成[重载]! → 它俩是[重写]!
Sub s = (Sub)base;
s.add(1,2,3); // "sub_2"
}
}
class Base {
public void add(int a, int... arr) {
System.out.println("base");
}
}
class Sub extends Base {
public void add(int a, int[] arr) {
System.out.println("sub_1");
}
public void add(int a, int b, int c) {
System.out.println("sub_2");
}
}
以上是关于12-面向对象5(多态)的主要内容,如果未能解决你的问题,请参考以下文章