Java的多态与实现
Java的多态与实现
概述
java实现多态有三个必要条件:继承、重写、向上转型(父类引用指向子类对象)。
1.可替换性(substitutability)。多态对已存在代码具有可替换性。例如,多态对圆Circle类工作,对其他任何圆形几何体,如圆环,也同样工作。
2.可扩充性(extensibility)。多态对代码具有可扩充性。增加新的子类不影响已存在类的多态性、继承性,以及其他特性的运行和操作。实际上新加子类更容易获得多态功能。例如,在实现了圆锥、半圆锥以及半球体的多态基础上,很容易增添球体类的多态性。
3.接口性(interface-ability)。多态是超类通过方法签名,向子类提供了一个共同接口,由子类来完善或者覆盖它而实现的。如图8.3 所示。图中超类Shape规定了两个实现多态的接口方法,computeArea()以及computeVolume()。子类,如Circle和Sphere为了实现多态,完善或者覆盖这两个接口方法。
4.灵活性(flexibility)。它在应用中体现了灵活多样的操作,提高了使用效率。
5.简化性(simplicity)。多态简化对应用软件的代码编写和修改过程,尤其在处理大量对象的运算和操作时,这个特点尤为突出和重要。
1.普通类
AnimalFu a = new AnimalZi new();
2.抽象类 animal 是父类抽象类
Animal a = new Cat();
3.接口 // 学生是类 smoking 是接口
Smoking sk = new Student()
多态的特点:
变量:编译看父类
方法:运行看子类
Java多态的实现机制是父类或接口定义的引用变量可以指向子类或实现类的实例对象,而程序调用的方法在运行期才动态绑定,就是引用变量所指向的具体实现对象的方法,也就是内存里正在运行的那个对象的方法,而不是引用变量的类型中定义的方法。
一、子类型和子类
只要是A类运用了extends关键字实现了对B类的继承,那么我们就可以说Class A是Class B的子类,子类是一个语法层面上的词,只要满足继承的语法,就存在子类关系。
子类型比子类有更严格的要求,它不仅要求有继承的语法,同时要求如果存在子类对父类方法的改写(override),那么改写的内容必须符合父类原本的语义,其被调用后的作用应该和父类实现的效果方向一致
对二者的对比是想强调一点:只有保证子类都是子类型,多态才有意义。Java父类强制转换子类原则
二、多态的机制
本质上多态分两种:
- 编译时多态(又称静态多态)
- 运行时多态(又称动态多态)
重载(overload)就是编译时多态的一个例子,编译时多态在编译时就已经确定,运行时运行的时候调用的是确定的方法。
我们通常所说的多态指的都是运行时多态,也就是编译时不确定究竟调用哪个具体方法,一直延迟到运行时才能确定。这也是为什么有时候多态方法又被称为延迟方法的原因。
三、多态通常有两种实现方法
- 子类继承父类(extends)
- 类实现接口(implements)
无论是哪种方法,其核心之处就在于对父类方法的改写或对接口方法的实现,以取得在运行时不同的执行效果。
要使用多态,在声明对象时就应该遵循一条法则:声明的总是父类类型或接口类型,创建的是实际类型。举例来说,假设我们要创建一个ArrayList对象,声明就应该采用这样的语句:
List list = newArrayList();
而不是
ArrayList list = newArrayList();
在定义方法参数时也通常总是应该优先使用父类类型或接口类型,例如某方法应该写成:
public void doSomething(List list);
而不是
public void doSomething(ArrayList list);
这样声明最大的好处在于结构的灵活性:假如某一天我认为ArrayList的特性无法满足我的要求,我希望能够用LinkedList来代替它,那么只需要在对象创建的地方把new ArrayList()改为new LinkedList即可,其它代码一概不用改动。
虚拟机会在执行程序时动态调用实际类的方法,它会通过一种名为动态绑定(又称延迟绑定)的机制自动实现,这个过程对程序员来说是透明的。
四、多态的用途
多态最大的用途我认为在于对设计和架构的复用,更进一步来说,《设计模式》中提倡的针对接口编程而不是针对实现编程就是充分利用多态的典型例子。
定义功能和组件时定义接口,实现可以留到之后的流程中。同时一个接口可以有多个实现,甚至于完全可以在一个设计中同时使用一个接口的多种实现(例如针对ArrayList和LinkedList不同的特性决定究竟采用哪种实现)。
五、多态的实现
继承实现多态的内部机制
在JVM执行Java字节码时,类型信息被存放在方法区中,通常为了优化对象调用方法的速度,方法区的类型信息中增加一个指针,该指针指向一张记录该类方法入口的表(称为方法表),表中的每一项都是指向相应方法的指针。
由于Java的单继承机制,一个类只能继承一个父类,而所有的类又都继承自Object类。方法表中最先存放的是Object类的方法,接下来是该类的父类的方法,最后是该类本身的方法。这里关键的地方在于,如果子类改写了父类的方法,那么子类和父类的那些同名方法共享一个方法表项,都被认作是父类的方法
注意这里只有非私有的实例方法才会出现,并且静态方法也不会出现在这里,原因很容易理解:静态方法跟对象无关,可以将方法地址直接引用,而不像实例方法需要间接引用。更深入地讲,静态方法是由虚拟机指令invokestatic调用的,私有方法和构造函数则是由invokespecial指令调用,只有被invokevirtual和invokeinterface指令调用的方法才会在方法表中出现。
由于以上方法的排列特性(Object——父类——子类),使得方法表的偏移量总是固定的。例如,对于任何类来说,其方法表中equals方法的偏移量总是一个定值,所有继承某父类的子类的方法表中,其父类所定义的方法的偏移量也总是一个定值。
前面说过,方法表中的表项都是指向该类对应方法的指针,这里就开始了多态的实现:
假设Class A是Class B的子类,并且A改写了B的方法method(),那么在B的方法表中,method方法的指针指向的就是B的method方法入口。而对于A来说,它的方法表中的method方法则会指向其自身的method方法而非其父类的(这在类加载器载入该类时已经保证,同时JVM会保证总是能从对象引用指向正确的类型信息)。结合方法指针偏移量是固定的以及指针总是指向实际类的方法域,我们不难发现多态的机制就在这里:
在调用方法时,实际上必须首先完成实例方法的符号引用解析,结果是该符号引用被解析为方法表的偏移量。
虚拟机通过对象引用得到方法区中类型信息的入口,查询类的方法表,当将子类对象声明为父类类型时,形式上调用的是父类方法,此时虚拟机会从实际类的方法表(虽然声明的是父类,但是实际上这里的类型信息中存放的是子类的信息)中查找该方法名对应的指针(这里用“查找”实际上是不合适的,前面提到过,方法的偏移量是固定的,所以只需根据偏移量就能获得指针),进而就能指向实际类的方法了。
实现接口多态的内部机制
事实上上面的过程仅仅是利用继承实现多态的内部机制,多态的另外一种实现方式:实现接口相比而言就更加复杂,原因在于,Java的单继承保证了类的线性关系,而接口可以同时实现多个,这样光凭偏移量就很难准确获得方法的指针。所以在JVM中,多态的实例方法调用实际上有两种指令:
- invokevirtual指令用于调用声明为类的方法;
- invokeinterface指令用于调用声明为接口的方法。
当使用invokeinterface指令调用方法时,就不能采用固定偏移量的办法,只能老老实实挨个找了(当然实际实现并不一定如此,JVM规范并没有规定究竟如何实现这种查找,不同的JVM实现可以有不同的优化算法来提高搜索效率)。
我们不难看出,在性能上,调用接口引用的方法通常总是比调用类的引用的方法要慢。这也告诉我们,在类和接口之间优先选择接口作为设计并不总是正确的,当然设计问题不在本文探讨的范围之内,但显然具体问题具体分析仍然不失为更好的选择。
总结
多态机制包括静态多态(编译时多态)和动态多态(运行时多态),静态多态比如说重载,动态多态是在编译时不能确定调用哪个方法,得在运行时确定。动态多态的实现方法包括子类继承父类和类实现接口。当多个子类上转型(不知道这么说对不)时,对象掉用的是相应子类的方法,这种实现是与JVM有关的。
子类Child继承父类Father,我们可以编写一个指向子类的父类类型引用,该引用既可以处理父类Father对象,也可以处理子类Child对象,当相同的消息发送给子类或者父类对象时,该对象就会根据自己所属的引用而执行不同的行为,这就是多态。即多态性就是相同的消息使得不同的类做出不同的响应。
其它知识补充
方法区和常量池。这两个都是Java虚拟在运行时的数据区域,常量池是方法区的一部分。
方法区
方法区与Java堆一样,是各个线程共享的内存区域,用于存储已经被虚拟机加载的类信息、常量、静态变
量、即使编译器编译后的代码等数据。
常量池
常量池是方法区的一部分,用于存放编译期所生成的各种字面量和符号引用。常量池*有11个常量表。
常量表类型 标志值(占1 byte) 描述
CONSTANT_Utf8 1 UTF-8编码的Unicode字符串
CONSTANT_Integer 3 int类型的字面值
CONSTANT_Float 4 float类型的字面值
CONSTANT_Long 5 long类型的字面值
CONSTANT_Double 6 double类型的字面值
CONSTANT_Class 7 对一个类或接口的符号引用
CONSTANT_String 8 String类型字面值的引用
CONSTANT_Fieldref 9 对一个字段的符号引用
CONSTANT_Methodref 10 对一个类中方法的符号引用
CONSTANT_InterfaceMethodref 11 对一个接口中方法的符号引用
CONSTANT_NameAndType 12 对一个字段或方法的部分符号引用
---------------------------------------------------------------------------------------------------
对于多态的原理主要研究方法区。在Java中多态指的是它允许基类的指针或引用指向派生类的对象,而在具体访问时实现方法的动态绑定。java中的方法调用有静态绑定和动态绑定之分,静态绑定指的是我们在编译期就已经确定了会执行那个方法的字节码,而动态绑定只有在运行时才能知晓。
静态绑定
Java中的静态方法、私有方法以及final修饰的方法的调用,都属于静态绑定,对于重载的实例方法的调用,也是采用静态绑定。静态绑定的原理主要是一个常量池解析的过程,下面来详解其过程:
假如有两个类A、B,在A类中我们调用了B类中的一个静态方法,在编译期,这个调用的动作会被编译成一条静态调用指令,该指令就对应常量池中的CONSTANT_Methodref表中所存储的该方法的符号引用,通过这个符号引用可以得到静态方法的全类名B,JVM加载B类,便会得到B类中方法的直接地址,该地址会被存储到A类常量池中对应的常量表中,这便是常量池解析过程,再次发起调用时,就会直接根据这个直接地址调用对应方法。以上过程可以看出,在编译阶段我们就已经确定了应该执行哪一个字节码代码。
动态绑定
动态绑定讲解之前需要了解JVM管理的一个重要的数据结构--方法表。它以数组的形式记录了当前类及其所有超类的可见方法字节码在内存中的直接地址 。
动态绑定前面的流程与静态绑定类似,假如此处有两个类A,B继承了A类,B类中重写了A类中的f1()方法,我们采用向上转型的方式用指向B实例的A类型引用调用f1()方法,编译器会生成一条字节码指令,该指令会去常量表中找到f1()方法信息的符号引用,通过该引用确定调用该方法的类型全名,即A类的全名称,根据名称加载到A类的字节码,去A类所对应的方法表中找到f1()方法,将它的直接地址记录到调用f1()方法的类的对应的常量表中,常量池解析结束,可以思考,我们此时是否能确定调用f1()方法时执行的是哪一块的字节码,答案是不能,因为截至此时我们f1()方法指定执行的是父类中的方法,引用虽然父类类型,但他指向的是父类对象还是子类对象是不知道的(确切地说是此时的程序不知道,程序员肯定是知道的),假如指向父类那就是父类中的f1()方法,如果指向子类的实例,子类没有重写,依然执行父类f1()方法,如果子类重写了就应该是子类的f1()方法。此时动态绑定就登场了,确定f1()换需要拿到B类实例在堆中的引用,通过引用找到堆中B的对象,根据对象进一步获取它的方法表,找到方法表的f1()方法的直接地址,此时便最终确定了。
所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用变量调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。
代码示例
基于继承实现的多态
public class Wine {
private String name;
public Wine(){
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String drink(){
return "喝的是 " + getName();
}
/**
* 重写toString()
*/
public String toString(){
return null;
}
}
public class JNC extends Wine{
public JNC(){
setName("JNC");
}
/**
* 重写父类方法,实现多态
*/
public String drink(){
return "喝的是 " + getName();
}
/**
* 重写toString()
*/
public String toString(){
return "Wine : " + getName();
}
}
public class JGJ extends Wine{
public JGJ(){
setName("JGJ");
}
/**
* 重写父类方法,实现多态
*/
public String drink(){
return "喝的是 " + getName();
}
/**
* 重写toString()
*/
public String toString(){
return "Wine : " + getName();
}
}
public class Test {
public static void main(String[] args) {
//定义父类数组
Wine[] wines = new Wine[2];
//定义两个子类
JNC jnc = new JNC();
JGJ jgj = new JGJ();
//父类引用子类对象
wines[0] = jnc;
wines[1] = jgj;
for(int i = 0 ; i < 2 ; i++){
System.out.println(wines[i].toString() + "--" + wines[i].drink());
}
System.out.println("-------------------------------");
}
}
OUTPUT-------------------------------
Wine : JNC--喝的是 JNC
Wine : JGJ--喝的是 JGJ
基于接口实现的多态
public class A {
public String show(D obj) {
return ("A and D");
}
public String show(A obj) {
return ("A and A");
}
}
public class B extends A{
public String show(B obj){
return ("B and B");
}
public String show(A obj){
return ("B and A");
}
}
public class C extends B{
}
public class D extends B{
}
public class Test {
public static void main(String[] args) {
A a1 = new A();
A a2 = new B();
B b = new B();
C c = new C();
D d = new D();
System.out.println("1--" + a1.show(b));
System.out.println("2--" + a1.show(c));
System.out.println("3--" + a1.show(d));
System.out.println("4--" + a2.show(b));
System.out.println("5--" + a2.show(c));
System.out.println("6--" + a2.show(d));
System.out.println("7--" + b.show(b));
System.out.println("8--" + b.show(c));
System.out.println("9--" + b.show(d));
}
}
output-------------------------------------------------
1--A and A
2--A and A
3--A and D
4--B and A
5--B and A
6--A and D
7--B and B
8--B and B
9--A and D
参考地址
https://blog.csdn.net/yz_cfm/article/details/85262909
https://blog.51cto.com/12222886/2069357
https://blog.csdn.net/weixin_42281036/article/details/81557468
https://zhuanlan.zhihu.com/p/69764885
上一篇: Redis 数据结构之zset有序集合