OOP语言中,多态是封装、继承之后的第三种基本特征。
封装:通过合并特征和行为来创建新的数据类型,“实现隐藏”通过细节“私有化”把接口和实现分离。
继承:以复用接口方式从已有类型用extends关键字创建新类型,并允许向上转型。
多态:消除类型之间的耦合关系(分离做什么和怎么做),基于继承的向上转型功能,允许同一种类型同一行为有不同的表现。
8.1再论向上转型
8.1.1忘记对象类型
不管导出类的存在,编写的代码(方法)只是针对基类类型。不需要为每个导出类型都写各自的代码,这正是多态所允许的。
8.2转机
程序运行时接受的是基类类型,但是它如何知道具体类型是哪一个从而调用正确的方法呢?我们需要了解绑定机制。
8.2.1方法调用绑定
把方法调用同方法主体关联起来称为绑定。
前期绑定:程序执行前绑定(由编译器和连接程序实现),C语言中方法调用都是前期绑定。
后期绑定:又叫动态绑定,运行时绑定,在运行时根据对象的类型绑定对应的方法主体。
Java中默认就是动态绑定,无需手动设置。特殊:static方法和final(private也是final)方法不存在多态性,不是动态绑定。
8.2.2产生正确行为
动态绑定使得多态中的基类对象可以正确执行相应的导出类对象方法。
8.2.3可扩展性
多态使得扩展新类型和扩展基类不会对已有代码(调用基类方法的代码)产生影响。它可以让程序员“将改变的事物与不变的事物分离开”。
8.2.4缺陷:不可以覆盖private方法
基类中private方法在子类中可以用相同的方法名和签名,但是它是一个全新的方法,不会按照我们想要的子类方法来执行。
调用的时候,按照基类方法的访问权限来决定是否可以调用。
子类是否会覆盖父类方法,按照子类是否可以访问到父类该方法来决定是否可以覆盖。
8.2.5缺陷:域与静态方法
多态特性(动态绑定)只是针对方法的。域和静态方法不具有这种特性。
如:父类和子类都有一个域 public String str; 在Super s = new Sub(); s.str 取出的是Super里的而不是Sub里的。 不过一般情况不会存在这种把域设置为public并且想用子类覆盖它的情况。
静态方法也不会有多态性。
8.3构造器和多态
构造器是隐式static方法,不具有多态特性。
8.3.1构造器的调用顺序
为什么编译器强制每个导出类的构造器必须调用基类构造器呢:因为构造器有个特殊的任务,检查对象是否被正确构造。导出类构造器只能访问它自己的成员,不能访问基类的成员(通常是private成员)。只有基类构造器才具有相应的知识和权限对自己的元素进行初始化。而导出类成员的初始化有可能会用到基类成员,因此导出类初始化在基类之后。
8.3.2继承与清理
通过组合和继承方式创建新类时,通常情况都是不需要担心对象的清理问题。
但是如果的确需要做清理时,必须非常小心谨慎:在使用完之后按照创建逆序清理,即sub.dispose()然后super.dispose()来清理。
更加复杂的情况:不知道什么时候使用结束,需要自己定义引用计数,然后再清理。
8.3.3构造器内部的多态方法行为
在调用子类构造器的过程中,会先调用父类构造器,此时子类构造器还没调用完成,子类对象也没有执行初始化,如果在父类构造器里调用多态方法,那么这个方法是可以产生多态行为特征的,但是由于子类构造器没有执行完,因此子类的初始化还没完成,多态方法里对子类成员变量的获取只能拿到默认值0,false,null
对象初始化过程(注意与类的加载过程区分):
1.给导出类对象分配内存空间,并初始化为0,false,null
2.调用父类构造器,并执行多态方法,拿到的是子类0,false,null的域
3.按照声明顺序调用成员变量的初始化
4.调用子类构造器主体
此处虽然逻辑没什么问题,但是行为的确错误了,所以在写构造器的时候,我们要尽可能用简单的方法使对象进入正常状态,如果可以的话避免调用其他方法。
8.4协变返回类型
导出类重写父类方法,方法的返回类型(区分返回值)可以是父类返回类型的某一个导出类。
8.5用继承进行设计
就创建新类型而言,不要只想到继承,应该优先考虑组合,它比继承具有更大的灵活性,可以动态的改变类型,而继承在编译时类型已经确定了。
8.5.1纯继承与扩展
纯粹的继承:基类接口与导出类完全一致,是is-a关系
扩展:导出类除了基类接口外,还有其他方法,是is-like-a关系,但是这些扩展方法不能以基类引用去调用
8.5.2向下转型与运行时类型识别
向上转型是安全的:基类不会有大于导出类的接口
向下转型需要确保类型的正确性:Java中类型转换(括号强转)都会进行类型检查,不正确抛出ClassCastException。这种在运行期间对类型进行检查的行为称作“运行时类型识别”(RTTI)。
8.6总结
多态意味着“不同的形式”。在OOP里,我们持有基类的相同接口,使用该接口的不同形式:不同版本的动态绑定。