Java编程思想 第八章:多态

OOP语言中,多态是封装、继承之后的第三种基本特征。

封装:通过合并特征和行为来创建新的数据类型,“实现隐藏”通过细节“私有化”把接口和实现分离

继承:以复用接口方式从已有类型用extends关键字创建新类型,并允许向上转型

多态:消除类型之间的耦合关系(分离做什么和怎么做),基于继承的向上转型功能,允许同一种类型同一行为有不同的表现

1. 再论向上转型

class Instrument{
	public Instrument() {
		//---
	}
	public void print(){
		System.out.println("Instrument-----:");
	}
}
class Wind extends Instrument{
	public void print(){
		System.out.println("Wind-----:");
	}
}
public class Music{
	public static void play(Instrument i){
		i.print();
	}
	public static void main(String[] args) {
		Wind wind = new Wind();
		play(wind);
	}
}

Music.play方法接受一个Instrument的引用,同时也接受任何Instrument类的子类,即main方法中play方法传递wind引用的时候,不需要做任何类型转换。这样做是允许的,因为Wind自Instrument类继承而来,所以Instrument类的接口必定存在于Wind类中。

我们可能觉得Music.play方法有点奇怪,既然要传递Wind引用,为什么不再写一个方法来传递Wind引用呢?当然,再写是可以的,只不过会使程序显得过为复杂而已,比如Instrument类有多个子类,那么就要写多个方法,显然没有这种单一的方法显得高效。因此只要我们记好这种特性那么就会使程序变得更加简洁。

2. 转机

2.1 方法调用绑定

把方法调用同方法主体关联起来称为绑定。

绑定可以分为前期绑定和后期绑定:

  • 前期绑定:程序执行前绑定(由编译器和连接程序实现),C语言中方法调用都是前期绑定。
  • 后期绑定:又叫动态绑定,运行时绑定,在运行时根据对象的类型绑定对应的方法主体。

Java中除了static和final方法(包括private)以外所有方法都是后期绑定

将某个方法声明为final,可以防止其他人覆盖该方法,“关闭”动态绑定,生成更有效的代码。大多数情况下并不会对程序的性能有什么提升,所以最好是根据设计而不是性能来使用final

2.2 产生正确的行为

动态绑定使得多态中的基类对象可以正确执行相应的导出类对象方法。

2.3 可扩展性

多态使得扩展新类型和扩展基类不会对已有代码(调用基类方法的代码)产生影响。它可以让程序员“将改变的事物与不变的事物分离开”。

2.4 缺陷:不可以覆盖private方法

基类中private方法在子类中可以用相同的方法名和签名,但是它是一个全新的方法,不会按照我们想要的子类方法来执行。调用的时候,按照基类方法的访问权限来决定是否可以调用。

子类是否会覆盖父类方法,按照子类是否可以访问到父类该方法来决定是否可以覆盖。

2.5 缺陷:域与静态方法

多态特性(动态绑定)只是针对方法的。域和静态方法不具有这种特性。

如:父类和子类都有一个域 public String str; 在Super s = new Sub(); s.str 取出的是Super里的而不是Sub里的。 不过一般情况不会存在这种把域设置为public并且想用子类覆盖它的情况。

静态方法也不会有多态性。

3.构造器和多态

构造器是隐式static方法,不具有多态特性。

3.1 构造器的调用顺序

为什么编译器强制每个导出类的构造器必须调用基类构造器呢:因为构造器有个特殊的任务,检查对象是否被正确构造。导出类构造器只能访问它自己的成员,不能访问基类的成员(通常是private成员)。只有基类构造器才具有相应的知识和权限对自己的元素进行初始化。而导出类成员的初始化有可能会用到基类成员,因此导出类初始化在基类之后。

3.2 继承与清理

通过组合和继承方式创建新类时,通常情况都是不需要担心对象的清理问题。

但是如果的确需要做清理时,必须非常小心谨慎:在使用完之后按照创建逆序清理,即sub.dispose()然后super.dispose()来清理。

更加复杂的情况:不知道什么时候使用结束,需要自己定义引用计数,然后再清理。

3.3 构造器内部的多态方法行为

在调用子类构造器的过程中,会先调用父类构造器,此时子类构造器还没调用完成,子类对象也没有执行初始化,如果在父类构造器里调用多态方法,那么这个方法是可以产生多态行为特征的,但是由于子类构造器没有执行完,因此子类的初始化还没完成,多态方法里对子类成员变量的获取只能拿到默认值0,false,null

对象初始化过程(注意与类的加载过程区分):

  1. 给导出类对象分配内存空间,并初始化为0,false,null
  2. 调用父类构造器,并执行多态方法,拿到的是子类0,false,null的域
  3. 按照声明顺序调用成员变量的初始化
  4. 调用子类构造器主体

4 协变返回类型

导出类重写父类方法,方法的返回类型(区分返回值)可以是父类返回类型的某一个导出类。

5 用继承进行设计

就创建新类型而言,不要只想到继承,应该优先考虑组合,它比继承具有更大的灵活性,可以动态的改变类型,而继承在编译时类型已经确定了。

一条通用的准则是:“用继承来表达行为之间的差异,并用字段来表达状态上的变化”。在上述列子中,两者都用到了:通过继承得到了两个不同的类,而运用组合使自己的状态发生变化。在这种情况下,这种状态的改变也就产生了行为的改变。

5.1 纯继承与扩展

纯粹的继承:基类接口与导出类完全一致,是is-a关系

在这里插入图片描述

扩展:导出类除了基类接口外,还有其他方法,是is-like-a关系,但是这些扩展方法不能以基类引用去调用

在这里插入图片描述

5.2 向下转型与运行时类型识别

向上转型是安全的:基类不会有大于导出类的接口

向下转型需要确保类型的正确性:Java中类型转换(括号强转)都会进行类型检查,不正确抛出ClassCastException。这种在运行期间对类型进行检查的行为称作“运行时类型识别”(RTTI)。

猜你喜欢

转载自blog.csdn.net/qq_21125183/article/details/84962506