8、多态
在面向对象的程序设计语言中,有三大特性,分别是封装、继承、多态。
多态分离了做什么和怎么做两个概念,做什么对应着接口,而怎么做对应着实现。
多态可以改善代码的组织结构和可读性,还能够创建可扩展的程序。
一个基类(父类)有许多不同的子类,在通过向上转型调用时,可以根据不同的子类,调用不同子类的方法。
8.1 再论向上转型
基类(父类)的指针指向子类的对象,就称之为向上转型。
class Father{
void speak(){
System.out.println("我是父类")
}
}
class Son extends Father{
void speak(){
System.out.println("我是子类")
}
}
public class Test{
public static void main(String[] args){
Father f = new Son();
f.speak();
}
}
// 输出结果
// 我是子类
Son类接受了一个Father类的引用,这以上代码中speak方法刚好也是Father类的方法,所以并不会报错。且输出的是子类的方法。
8.1.1 忘记对象类型
class Father{
void speak() {System.out.println("我是父类");}
}
class Son extends Father{
void speak() {System.out.println("我是子类");}
void play() {System.out.println("我是子类");}
}
public class TestString{
public static void main(String[] args){
Father f = new Son();
f.play();//报错!!!
}
}
以上的代码会报错,因为Father类中并没有play方法。
所以作者让我们忘记对象类型,忘记他是个Son类,只需要知道他是个Father类,它只能执行Father类中的方法。
8.2 转机
Father f = new Son();
f.speak();
编译器如何知道Father f指向的是Son对象,而不是其他的对象。实际上,编译器无法得知。
为了深入理解这个问题,有必要研究一下绑定
这个话题。
8.2.1 方法调用绑定
将一个方法调用 与 一个方法主体 关联起来被称为绑定。即方法名和执行的具体方法进行绑定。
前期绑定时面向过程语言的默认绑定方式。
后期绑定则意味着运行时根据对象的类型进行绑定。后期绑定又称动态绑定或运行时绑定。
Java中除了static方法和final方法(private方法属于final方法)之外,其他所有的方法都是后期绑定。
8.2.2 产生正确的行为
一旦知道Java中所有方法都是通过绑定实现多态这个事实后,就可以编写只与基类打交道的程序代码了。
class Shape{
public void draw(){}
}
class Circle extends Shape{
public void draw(){
System.out.println("Circle");
}
}
public Square extends Shape{
public void draw(){
System.out.println("Square");
}
}
public class Test{
public static void main(String[] args){
Shape a = new Circle();
Shape b = new Square();
a.draw();
b.draw();
}
}
// 输出结果
// Circle
// Square
8.2.3 可扩展性
由于多态机制,我们可以根据自己的需求对系统添加任意多的新类型,而不需要更改draw()方法。
8.2.4 缺陷:无法“覆盖”私有方法
对于基类(父类)的私有方法,子类是无法通过多态进行覆盖的。
public class Father{
private void f(){
System.out.println("私有的f方法");
}
public static void main(String[] args){
Father t = new Son();
t.f();
}
}
class Son extends Father{
public void f(){
System.out.println("公有的f方法");
}
}
// 输出结果
// 私有的f方法
### 8.2.5 缺陷:域与静态方法
只有普通方法可以多态
对象的属性,对象的静态方法 是不可以进行多态的
class Super{
public int field = 0;
public static int f(){ return 0;}
}
class Sub extends Super{
public int field = 1;
public static int f(){ return 1;}
}
public class Test{
public static void main(String[] args){
Super s = new Sub();
System.out.println(s.field);
System.out.println(s.f());
}
}
// 输出结果
// 0
// 0
8.3 构造器(构造函数)和多态
构造函数不具有多态性,因为他实际上是static方法,只不过该static声明是隐式的。
但是还是很有必要了解多态如何在复杂的层次中运作。
class Meal{
Meal(){ System.out.println("Meal()"); }
}
class Bread{
Bread(){ System.out.println("Bread()"); }
}
class Cheese{
Cheese(){ System.out.println("Cheese()"); }
}
class Lettuce{
Lettuce(){ System.out.println("Lettuce()"); }
}
class Lunch extends Meal{
Lunch(){ System.out.println("Lunch()"); }
}
class PortableLunch extends Lunch{
PortableLunch(){ System.out.println("PortableLunch()"); }
}
public class Sandwish extends PortableLunch{
private Bread b = new Bread();
private Cheese c = new Cheese();
private Lettuce l = new Lettuce();
public Sandwish(){System.out.println("Sandwich()");}
public static void main(String[] args){
new Sandwich();
}
}
// 输出结果
// Meal()
// Lunch()
// PortableLunch()
// Bread()
// Cheese()
// Lettuce()
// Sandwich()
如上图所示,SandWich继承了PortableLunch,ProtableLunch继承了Lunch,Lunch继承了Meal。
执行顺序如下
(1)调用父类构造器,父类再调用父类,反复递归下去
(2)初始化当前类的成员
(3)调用当前类的构造方法
8.3.2 继承与清理
通过组合和继承方式创建新类,不需要担心对象的清理问题。子类会留给垃圾回收器进行处理。
如果必须要清理,就需要为类创建dispose()方法。需要重写基类的dispose()方法(其中的dispose是作者自己设定的,你也可以设计自己的名称)
8.3.3 构造器内部的多态方法的行为
如果在构造函数中调用一个多态方法(动态绑定的方法),会发生什么?
多态方法,一般是在运行时才可以决定具体的方法体,如果在构造方法中调用,会出现难以发现的错误。
虽然不会报错,但会导致变量为设置初始值,从而出现意想不到的错误。
class Glyph{
void draw(){ System.out.println("Glyph");}
Glyph(){
System.out.println("Glyph before");
draw();
System.out.println("Glyph after");
}
}
class RoundGlyph extends Glyph{
private int radius = 1;
RoundGlyph(int r){
radius = r;
System.out.println("RoundGlyph, radius="+radius);
}
void draw(){
System.out.println("RoundGlyph, radius="+radius)
}
}
public class Test{
public static void main(String[] args){
new RoundGlyph(5);
}
}
// 输出结果
// Glyph before
// RoundGlyph, radius=0
// Glyph after
// RoundGlyph, radius=5
从以上的代码发现,radius从未设置为0,结果却输出为0,出现了意想不到的错误,所以不要在构造器中使用多态方法
8.4 协变返回类型
Java SE5中添加了协变返回类型,它表示在导出类(子类)中被覆盖方法可以返回基类(父类)方法的返回类型的某种子类类型。
class Grain{
public String toString(){return "Grain";}
}
class Wheat extends Grain{
public String toString(){ return "Wheat"; }
}
class Mill{
Grain process(){ return new Grain(); }
}
class WheatMill extends Mill{
Wheat process(){ return new Wheat(); } //这是一个多态方法!!!,尽管返回值不一样,但是有继承关系
}
public class Test{
public static void main(String[] args){
Mill m = new Mill();
Grain g = m.process();// 返回Grain类型
System.out.println(g);
m = new WheatMill();
g = m.process(); // 返回Wheat类型,也是可以的。
System.out.println(g);
}
}
// 输出结果
// Grain
// Wheat
SE5之前的版本,子类覆盖process必须强制返回Grain,而不是Wheat。
SE5之后,解除了这个限制。
8.5 用继承进行设计
在考虑使用继承还是组合的时候,请优先使用组合,因为组合更加的灵活。
继承在编译的时候需要知道确切的类型。
8.5.1 纯继承与扩展
采取纯粹的方式创建继承结构似乎是最好的方式。也就是说,只有在基类中已经建立的方法才可以在导出类中被覆盖。
让继承的子类完全替代基类(父类),并且继承的子类不需要任何额外的信息,即不需要创建除了父类以外的任何方法。所以,存粹的is-a才是继承的最好选择。
8.5.2 向下转型与运行时类型识别
由于向上转型
会丢失具体的类型信息,所以需要通过向下转型
来获得具体的类型信息。
class Father{
public void f(){}
public void g(){}
}
class Son{
public void f(){}
public void g(){}
public void h(){}
}
public class Test{
public static void main(String[] args){
Father f1 = new Son();
f1.f();//正常执行
f1.h();//报错
((Son)f1).h(); //强制转换为Son类型之后,可以执行
}
}
8.6总结
多态意味着不同的形式。从基类继承而来的相同接口,以及使用该接口的不同形式,不同版本的动态绑定方法。
多态是在继承和数据抽象的基础上实现的。