Ⅷ 多态
多态主要将做什么和怎么做分离开来,从而改善了代码的组织结构和可读性。多态可以帮助你创建可拓展的程序,在开发的后期和维护阶段,可以较方便地添加或修改功能。
多态主要是为了消除类型间的耦合关系。
8.1 再论向上转型
先写一个向上转型的例子:
class Animal{
private String name;
public void eat(){
System.out.print("Animal eat");
}
}
class Mammalian extends Animal{
private String name;
@override
public void eat(){
System.out.print("Mammalian eat");
}
}
class Reptiles extends Animal{
private String name;
@override
public void eat(){
System.out.print("Reptiles eat");
}
}
//测试类
public class Test{
//主函数
public static void main(String[] args){
Mammalian m=new Mammalian();
Reptiles r=new Reptiles();
Animal[] array_ani={new Mammalian(),new Reptiles()};
active(array_ani[0]);//输出Mammalian eat
active(array_ani[1]);//输出Reptiles eat
}
//成员函数
public static void active(Animal a){
a.eat();
}
}
Test.active()
接收Animal的引用,那么同时接收任何Animal的子类,而这不需要经过任何类型转换。Mammalian向上转型到Animal的过程中,是安全的,只是可能会缩小接口。
8.1.1 忘记对象类型
在上述代码中,Test.active()
似乎故意忘记了参数的类型。假如没有多态,那么需要为每一个Animal类型的引用编写一个Test.active()
(顺带一提,此时同名的active函数为重载),而且意味着,当我们想添加一个针对Animal类型的,类似于active的方法时,还需要做大量的工作。这是相当麻烦且没有必要的。
8.2 转机
那么应该大家都会有一个疑惑,为什么active方法能忘记参数类型,怎么才能让编译器知道这是一个指向Mammalian,而不是Animal的引用呢?实际上,编译器并不知道。这和绑定有关。
8.2.1 方法调用绑定
将一个调用同一个方法体关联起来被称为绑定。在方法被调用之前,由编译器和连接程序实现的绑定被称为前期绑定(比如C语言等一些面向过程的语言只有只有这一种默认绑定方式)
解决的方式是后期绑定,也被称为动态绑定和运行时绑定。比如上文中的active方法,它是在运行时,根据对象的类型来进行绑定。而这一切,编译器并不知道,而是一种方法调用机制能找到正确的方法体。那么问题又来了,这种调用机制是怎么找到正确的方法体的呢?这是在对象中安置了某种“类型信息”。
在Java中,除了static和final方法(private为隐式final),其余所有方法都是采用了后期绑定。
8.2.4 缺陷:“覆盖”私有方法
基类中的private
修饰的方法,因为是隐式的final
方法,所以是不可继承的。即使在子类中写了一个同名方法,那相当于是一个全新的方法。基类中只有非private
方法才能被覆盖。
8.2.5 缺陷:域与静态方法
只有普通的方法调用才是多态的,直接访问某个域,这将会在编译期进行。
之前也提到过了,静态方法的行为不具有多态,因为静态方法是与类,而非单个对象相通信的。
8.3 构造器和多态
构造器其实是隐式的static
方法,所以构造器不具备多态。不过还是需要理解构造器怎么样通过多态在复杂的层次结构中运作,这一理解将有助于大家避免一些困扰。
8.3.1 构造器的调用顺序
为什么构造子类时要调用基类构造器?书中这一段话已经写的很好了:
基类的构造器总是在导出类的构造过程中被调用,而且按照继承层次逐渐向上链接,以使每个基类的构造器都能得到调用。这样做是有意义的,因为构造器具有一项特殊任务:检查对象是否被正确构造。导出类只能访问自己的成员,不能访问基类成员(基类成员通常都是private的)只有基类的构造器才具有恰当的知识和权限来对自己的元素进行初始化。因此,必须令所有的构造器都得到调用否则就不可能正确构造完整对象。
构造顺序:
- 调用基类构造器。这个步骤会递归下去,首先是根,然后是下一层导出类,接着继续。
- 按声明顺序调用成员的初始化方法。
- 调用导出类构造器的主体。
8.3.2 继承与清理
对象通常将由垃圾回收器来处理,不过当我们需要进行一些必要的手动清理工作时,需要注意一点(假如我们定义了一个清理方法为dispose()
):因为执行子类的dispose()
会覆盖基类的dispose()
,所以需要在子类中显式地执行super.dispose()
才能完成基类地清理工作。而且需要注意调用顺序,构造时是由上而下的,清理时就是由下而上的。
8.3.3 构造器内部的多态方法的行为
这一小节有点难理解,先跳过。
8.5.2 向下转型与运行时类型识别
向上转型是安全的,但是会丢失具体的类型信息。类似的,我们猜想,向下转型应该能获取类型信息,但是这该怎么获取呢?
首先我们要保证向下转型的正确性。在某些程序设计语言中,比如C++,必须执行一些特殊的操作来保证安全的向下转型。在Java中,所有转型都会得到检查,在进入运行时仍会对其进行检查,以保证他的确是我们需要的类型。若检查到不是,则抛出类转型异常。这些操作 被称为RTTI(运行时类型识别)
8.6 总结
如果不是数据抽象的继承,就不可能理解和运用多态。所以多态不是一个孤立的特性。
为了有效地运用多态乃至面向对象,必须拓展自己的编程视野,将类与类的共同特性和和相互之间的关系联系起来。这是一个渐趋成熟的过程,它将有效缩短开发过程,更好地组织代码,更容易拓展和更方便地维护。
扫一扫,关注公众号
小白的成长探索之路。
网友评论