06_0 继承
1.继承
继承是允许重用现有类来构造新类的特性
继承是向下传递的
- 子类所具有的数据和行为总是作为与其相关的父类的属性的扩展(extension)(即更大的集合)。子类具有父类的所有属性以及其他属性。
- 继承总是向下传递的,因此一个类可以从它上面的多个超类中继承各种属性 。
如果Dog是Mammal的派生类,而Mammal又是Animal的派生类,则Dog不仅继承了Mammal的属性,同时也继承了Animal的属性。
- 派生类可以覆盖从基类继承来的行为。
继承的作用
- 使代码具有可重用性——通过继承类库拥有类库的能力
用别人的类为基础创建自己需要的类,可以省时省力
- 可以重定义基类的成员函数,实现新的功能
对别人的类中的不足重新实现,但是又不改变别人的类
- 向派生类添加新成员,实现功能发展和扩展
基于对别人的类的基础上,实现新的类
- 不需要了解核心技术的细节,就能拥有别人的能力
简单地继承别人的类,可以不需要了解其功能的实现细节,就可以直接使用
继承关系的判断
- 当类与类之间存在着所属关系时,才具备了继承的前提。a是b中的一种。a继承b。狼是犬科中的一种。
- 所属关系:" is a "
- 注意:不要仅仅为了获取其他类中的已有成员进行继承。
- 判断所属关系:如果继承后,被继承的类中的功能,都可以被该子类所具备,那么继承成立。如果不是,不可以继承。
2.基类和派生类
派生类的声明必须指定基类的名称
class Manager extends Employee
基类分为两种类型:
直接基类
间接基类
直接基类和间接基类
直接基类:
class B extends class A
//A是B的直接基类
间接基类:
class B extends class A
class C extends class B
//A是C的间接基类
3.继承的层次
单一继承
- 只拥有一个父类的新类的创建过程
- 从一个现有基类创建新类的过程
- 使用一个箭头从派生类指向基类
- 表示派生类引用基类的函数和数据,而基类没有访问派生类的权限
JAVA为什么不支持多重继承
因为当一个类同时继承两个父类时,两个父类中有相同的功能,则子类对象调用该功能时,无法确定运行的是哪一个?
- 但是java支持多级继承。A继承B B继承C C继承D。
- 多级继承的出现,就有了继承体系。
- 体系中的顶层父类是通过不断向上抽取而来的。它里面定义的该体系最基本最共性内容的功能。
多级继承
层次继承
多重继承
4.访问控制
派生类的函数
能够访问基类的保护和公有成员
派生类的对象
通过派生类的对象能够访问基类的公有成员
通过派生类的对象不能访问基类的私有和保护成员
5.继承的形式
- 特殊化(specialization)继承
- 规范化(specification)继承
- 构造(Construction)继承
- 泛化继承
- 扩展继承
- 限制继承
- 变体继承
- 合并继承 (多重继承)
6.特殊化继承
- 通过**(is-a)检验规则**检验两个概念是否为继承关系
- 如果检验概念 A 与概念 B 是否为继承关系,那么就尝试着套用这个英语语句:“A(n) A is a(n) B”,如果这个语句“听起来是对的”,那么这个继承关系很可能就是正确的。
在静态类型语言中:
- 子类实例必须拥有父类的所有数据成员。
- 子类的实例必须至少通过继承实现父类所定义的所有功能。
- 这样,在某种条件下,如果用子类实例来替换父类实例,那么将会发现子类实例可以完全模拟父类的行为,二者毫无差异。
可替换原则(重要)
指如果类B是类A的子类,那么在任何情况下都可以用类B来替换类A,而外界毫无察觉。
子类型
- 指符合替换原则的子类关系。
- 区别于一般的可能不符合替换原则的子类关系
- 子类说明了新类是继承自父类,而子类型强调的是新类具有父类一样的行为
特殊化继承
- 很多情况下,都是为了特殊化才使用继承。
- 在这种形式下,新类是基类的一种特定类型,它能满足基类的所有规范。 用这种方式创建的总是子类型,并明显符合可替换性原则。
- 与规范化继承一起,这两种方式构成了继承最理想的方式,也是一个好的设计所应追求的目标。
(里氏)替换(代换)原则
- 类B继承类A时,除添加新的方法完成新增功能外,尽量不要重写父类A的方法,也尽量不要重载父类A的方法
- 含义:父类中凡是已经实现好的方法(相对于抽象方法而言),实际上是在设定一系列的规范和契约,虽然它不强制要求所有的子类必须遵从这些契约,但是如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏
替换原则
子类可以扩展父类的功能,但不能改变父类原有的功能
- 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
- 子类中可以增加自己特有的方法。
- 当子类的方法重写父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
- 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
改写(重写)
- 子类有时为了避免继承父类的行为,需要对其进行改写
- 语法上:子类定义一个与父类有着相同名称且类型签名相同的方法。
- 运行时:变量声明为一个父类,它所包含的值来自于子类,与给定消息相对应的方法同时出现于父类和子类。
改写(重写)机制
- Java、Smalltalk等面向对象语言,只要子类通过同一类型签名改写父类的方法,自然便会发生所期望的行为。
- C++中,需要父类中使用关键字Virtual来表明这一含义。
方法(函数)改写
- 为了能使基类中过时的方法能被修改,我们可以在派生类中定义与基类中完全相同的函数,这叫做方法(函数)改写(重写、重置、覆盖)
派生类的函数覆盖基类的同名函数
- 通过派生类的对象调用时,执行派生类的函数
- 用基类的对象调用时,执行基类的函数
子类型与改写
子类型比子类有更严格的要求,它不仅要求有继承的语法,同时要求如果存在子类对父类方法的改写(override),那么改写的内容必须符合父类原本的语义,其被调用后的作用应该和父类实现的效果方向一致。
7.规范化继承
- 规范化继承用于保证派生类和基类具有某个共同的接口,即所有的派生类实现了具有相同方法接口的方法。
- 基类中既有已实现的方法,也有只定义了方法接口、留待派生类去实现的方法。
- 派生类只是实现了那些定义在基类却又没有实现的方法。
- 派生类并没有重新定义已有的类型,而是去实现一个未完成的抽象规范。 也就是说,基类定义了某些操作,但并没有去实现它。只有派生类才能实现这些操作。
- 在这种情况下,基类有时也被称为抽象规范类。
- 在Java中,关键字abstract确保了必须要构建派生类。声明为abstract的类必须被派生类化,不可能用new运算符创建这种类的实例。除此之外,方法也能被声明为abstract,同样在创建实例之前,必须覆盖类中所有的抽象方法。
- 规范化继承可以通过以下方式辨认:基类中只是提供了方法接口,并没有实现具体的行为,具体的行为必须在派生类中实现。
8.构造继承
- 子类使用父类提供的行为,但并不是父类的子类型
- 子类通过继承父类就可以实现其需要的行为,只需要对方法名称、方法参数等进行修改
9.泛化继承
- 子类修改或改写父类的某些方法
10.拓展继承
- 子类只是往父类中添加新行为,并不修改从父类继承来的任何属性
- 由于父类的功能仍然可以使用,而且没有被修改,因此扩展继承并不违反可替换性原则,用这种方式构建的子类还是子类型
11.限制继承
- 如果子类的行为比父类的少或更严格,就出现了限制继承
12.变体继承
子类和父类之间都是对方的变体,可以任意选择两个之间的父子关系
例如,用来控制鼠标的代码与用来控制图形输入板的代码几乎完全相同。在概念上没有理由让一个类作为另外一个类的父类,因此可以选择任何一个类作为另外一个类的父类
13.合并继承
- 可以通过合并两个或者更多的抽象特性来形成新的抽象。
- 一个类可以继承自多个基类的能力被称为多重继承。
14.继承的形式
- 特殊化:子类是父类的一个特例,即子类是父类的一个子类型
- 规范化:父类中定义的行为在子类中实现,而父类本身没有实现这些行为
- 构造:子类使用父类提供的行为,但是并不是父类的子类型。
- 泛化:子类修改或改写父类的某些方法
- 扩展:子类添加一些新功能,但没有改变继承来的行为
- 限制:子类限制了一些来自父类的方法的使用
- 变体:子类和父类之间都是对方的变体,可以任意选择两个之间的父子关系
- 合并:子类从多个父类中继承特性
15.构造函数的继承
子类构造方法
- 构造器不能被继承
子类不能继承父类的构造器
- 构造器只能通过两种方法获得:
自己编写;
使用系统默认构造器。
子类构造方法
- 调用父类构造器
- 调用父类构造器必须在第一行使用super();
- 如果没有使用this ()或super(),系统将自动调用super();
- 如果系统调用super(),而父类没有super(),则出错。
- 如果一个类没有定义构造函数,编译器默认会为该类插入一个默认的无参构造函数。
- 子类中,父类构造函数必须由程序员显式调用或由编译器隐式地调用。
例子:
class SuperClass{
private int n;
SuperClass(){
System.out.println("SuperClass()");
}
SuperClass(int n){
System.out.println("SuperClass(int n)");
this.n = n;
}
}
class SubClass extends SuperClass{
private int n;
SubClass(){
super(300);
System.out.println("SuperClass");
}
SubClass(int n){
System.out.println("SubClass(int n):"+n);
this.n = n;
}
}
public class TestSuperSub{
public static void main (String args[]){
SubClass sc = new SubClass();
SubClass sc2 = new SubClass(200);
}
}
结果:
如果把SuperClass()删除,会发现报错了
如果一个类有了构造函数,系统是不会默认为它生成一个空的构造函数的
多级继承的调用次序
class A {
A() {
System.out.println("A");
} }
class B extends A {
B() {
System.out.println("B"); } }
class C extends B {
C() { System.out.println("C"); } }
public class hrt {
public static void main(String args[]) {
new C();
}
}
结果:
A
B
C
基类构造器总是在导出类的构造过程中被调用,而且按照继承层级逐渐向上链接(调用顺序则是从基类开始向下)。可以理解为,这么做的逻辑关系是在一个类构建时可能会用到其父类的成员、方法。在清理时顺序相反。
END