第8章 多态(Polymorphism)
OOP语言的三个基本特征:数据抽象、继承、多态。多态也叫做动态绑定、后期绑定、运行时绑定。
多态是什么意思呢?多态是发生在继承中的。意思是 代码中定义的 引用变量 所指向的具体 类 和变量调用的 具体方法 在 程序运行时才可以确定。
8.1 再讨论向上转型
看一段代码
这里书上给了一个例子。意思是如果在方法调用时候,参数不写基类引用而写了子类引用,就需要对每一个子类都写其对应类型的方法。而如果写基类引用,就不会存在这个问题,UPcasting会将子类自动转换为基类,所以只需要写对于基类应用的方法.
// 参数写基类引用
方法A(Instrument i){
// 方法实现
}
// 参数写子类引用,这里的 Bass Wind Stinged 是 Instrument的子类
方法B(Bass b){
}
方法C(Wind w){
}
方法D(Stringed s){
}
这里需要对三种方法都写一遍。
就是说这里可以用向上转型把所有子类变量的情况都转成基类变脸的情况,只写一个方法。
8.2 转机
在上面的例子代码中,如果我们向上转型实现了。那么编译器这么知道方法A的参数引用 i 到底是 Bass,Wind还是 Stringed呢?实际上,编译器根本不知道(很好理解,只能向上转,唯一;向下不唯一,不能转)。下面我们研究一下“绑定”这个话题。
8.2.1方法调用绑定
将一个方法调用和一个方法主体关联起来称作绑定。
- 前期绑定:若在程序执行前进行绑定(由编译器和连接程序实现),叫做前期绑定。
- 后期绑定:在运行时根据对象的类型进行绑定。后期绑定也叫做 动态绑定 或 运行时绑定。想要实现后期绑定,就必须有某种机制,使得运行时可以判断对象的类型,从而调用恰当的方法。
- 再谈final方法:前面第7章提到过,final方法是不允许别人修改(覆盖)方法。同时,也可以“关闭”动态绑定,告诉编译器不需要对其进行动态绑定。理论上可以提高效率,不过实际没啥用。所以final方法主要是出于设计,即不能修改。
8.2.2 产生正确的行为
意思是说,虽然向上转型发生时,并不能知道具体的类型是什么,但是由于方法调用绑定,总可以调用到正确的导出类的方法。
书上给了一个例子,如下面的代码,当调用 Generator.objectGenerator() 方法,生成对象引用,由于向上转型,这里的对象时 Shape 类,并且不能够知道到底是 Circle 类还是 Triangle类,但是该对象依然可以调用Circle或者Tri中的方法。
记住一点,虽然发生了 upcasting,但是可以调用基类的方法。
public class Shape(){
public void draw(){"Shape.draw()";}
public void erase(){"Shape.erase()";}
}
class Circle extends Shape{
public void draw(){"Circle.draw()";}
public void erase(){"Circle.erase()";}
}
class Tirangle extends Shape{
public void draw(){"Triangle.draw()";}
public void erase(){"Triangle.erase()";}
}
public Generator{
public Shape objectGenerator(int i){
if(i==1) return new Circle();
if(i==2) return new Tirangle();
}
}
8.2.3 可扩展性
这里没什么意思,代码就是这样的,就应该是这样的。具体可以见书上的例子。
8.2.4 缺陷:“覆盖” 私有方法
书上给出了这样的例子。可以看到,我们想要调用的是导出类的 f() 方法,但是实际上却调用了基类的 f() 方法。原因就是 private 方法由于自动被设置成为 final,在导出类中是不可访问的。所以在导出类中写的 f() 方法是一个新的方法。
这里不要疑惑,继承类是继承所有的接口和方法。(继承也不能访问private变量)
记住,在导出类中,对于基类中的 private 方法,最好采用不同的名字,不然调用的方法就还是基类中的方法。
package polymorphism;
// 这里是说 多态的 覆盖 私有方法问题
class Derived extends PrivateOverriding{
public void f(){System.out.println("public f()");}
}
public class PrivateOverriding {
private void f(){System.out.println("private f()");}
public static void main(String args[]){
PrivateOverriding po = new Derived();
po.f();
}
}
/*output
private f()
*/
8.2.5 缺陷:域与静态方法
这里想说的是,访问方法可以是 多态 的,但是直接访问域却不是。看下面这个例子:第一行输出中,直接访问sup.field 这个变量,不是多态的,相当于直接访问父类对象的 field 这个数据成员。而访问 sup.getField() 方法时,多态起作用,访问到了导出类的 getField() 方法。<这里书上写的很全面,upcasting 发生时给sub.field 和 super.field 不同的存储空间,访问的时候没有多态,就访问到了sub.field空间,大白话就是前面写的这样。>
package polymorphism;
class Super{
public int field = 0;
public int getField(){return field;}
}
class Sub extends Super{
public int field = 1;
public int getField(){ return field;}
public int getSuperField() { return super.field;}
}
public class FieldAccess {
public static void main(String args[]){
Super sup = new Sub(); // upcasting
System.out.println("sup.field = "+ sup.field+", sup.getField() = "+ sup.getField());
Sub sub = new Sub();
System.out.println("sub.field = "+ sub.field+", sub.getField() = "+ sub.getField()
+", sub.getSuperField() = " + sub.getSuperField());
}
}
/*output
sup.field = 0, sup.getField() = 1
sub.field = 1, sub.getField() = 1, sub.getSuperField() = 0
*/
除了上面代码中的问题,还有对于静态方法的访问也不是多态的。
package polymorphism;
class StaticSuper{
public static String staticGet(){
return "Base staticGet()";
}
public String dynamicGet(){
return "Base dynGet()";
}
}
class StaticSub extends StaticSuper{
public static String staticGet(){
return "Derived staticGet()";
}
public String dynamicGet(){
return "Derived dynGet()";
}
}
public class StaticPoly {
public static void main(String argg[]){
StaticSuper sup = new StaticSub(); // upcast
System.out.println(sup.dynamicGet());
System.out.println(sup.staticGet());
}
}
/* output
Derived dynGet()
Base staticGet()
*/
可以看到上面的代码里面,对象做了 upcast 之后,访问 dynGet() 方法时候,产生了多态(绑定),即访问到了导出类的相关方法。而访问 static 方法 staticGet(),没有多态,访问到的是基类的相关方法。
总结一下,上面的两种情况是说:对于类中的 变量域 和 static 方法,是不能产生多态的。
8.3 构造器和多态
8.3.1 构造器的调用顺序
之前在第5章中讲过构造器的调用顺序,当时说类内初始化顺序是先静态变量,再非静态变量,再构造器。这里我们结合多态再来看一下。
package polymorphism;
class Meal{
Meal(){ System.out.println("Meal()");}
}
class Bread{
Bread(){System.out.println("Bread()");}
}
class Cheese{
Cheese(){System.out.println("Cheese()");}
}
class Lettuce{ /* letis 生菜 */
Lettuce(){System.out.println("Lettuce()");}
}
class Lunch extends Meal{
Lunch(){System.out.println("lunch()");}
}
class PortableLunch extends Lunch{
PortableLunch(){System.out.println("PorLunch()");}
}
public class Sandwich extends PortableLunch{
private Bread bread = new Bread();
private Cheese cheese = new Cheese();
private Lettuce lettuce = new Lettuce();
public Sandwich(){System.out.println("Sandwich()");}
public static void main(String args[]){
new Sandwich();
}
}
/*output
Meal()
lunch()
PorLunch()
Bread()
Cheese()
Lettuce()
Sandwich()
*/
当含有继承行为时,初始化顺序是:
- 基类构造器
- 和第5章说过一样的顺序
在逻辑上来说,由于导出类要继承基类的所有接口,所以要先对基类对象成员进行构建,执行基类构造器。之后,在执行导出类构造器之前,需要对导出类的所有对象进行初始化。体现在顺序上就是上面的这样。
和第5章的提到的构造器初始化顺序结合:
- 基类构造器
- 导出类对象(先静态)
- 导出类构造器
8.3.2 继承与清理
8.3.3 构造器内部的多态方法的行为
这里说了一个问题,在构造器内部调用正在构造对象的某个绑定方法,会发生错误(该方法操作的成员可能还未初始化)。具体可以看一下书上的代码,这里就不写了。
所以给出的建议是,在构造器内部,安全的调用是 基类中的 final 方法(private方法自动被定义为 final)。
这个点是很细节的问题,可能个人都不会遇到。
需要记住,在构造器内部调用基类方法时候,可能会调用到导出类的重写方法,会遇到问题。
8.4 协变返回类型
java SE5 中添加了这个概念。 表示,导出类中的被覆盖方法,可以返回 基类方法的返回类型的某种导出类型。
知道有这个词就行了。
8.5 用继承进行设计
没什么说的。
网友评论