第4章 类的继承
计算机程序经常使用类之间的继承关系来表示对象时间的分类关系。在继承关系中,有父类和子类,父类也叫基类,子类也叫派生类。子类继承了父类的属性和行为,而子类也可以增加子类特有的属性和行为。对于某些父类有的行为,子类的实现方式可能和父类也不完全一样。
使用继承一方面可以复用代码,公共的属性和行为可以放到父类中,而子类只需关注子类特有的部分就可以了。另一方面,不同子类的对象可以更为方便地被统一处理。
4.1 基本概念
在Java中,所有类默认都有一个父类,Object。
Object没有定义属性,但定义了一些方法,如toString()
,getClass
,hashCode()
等。
Java的继承有以下要求。
- Java使用
extends
关键字来表示继承关系,一个类最多只能由一个父类。 - 子类不能访问父类的
private
属性和方法。 - 子类继承了除
private
外的父类的所有属性和方法。
子类可以重写父类的方法,以反映自己不同的实现。重写实际上是在子类中定义和父类一样的方法,然后重新实现。而重写方法时,需要在重写的方法前加上@Override
,以表示子类的这个方法是重写的父类的方法。
在使用new
创建对象的过程中,父类的构造方法也会执行,且优先于子类的构造方法调用。如果需要显式地调用父类的构造方法,则需要使用super
关键字。super
的使用方法和this
相同,只不过super
代表的是父类。但是,this
引用一个对象,是实实在在存在的,可以作为函数参数,可以作为返回值。但super
只是一个关键字,不能作为参数和返回值,其作用只是告诉编译器访问父类的相关变量和方法。
使用继承的一个好处是通过一个父类变量来统一处理不同子类型的对象。这被称为多态,我们可以将一个子类变量给一个父类变量,如果父类具有多个子类。此时,通过父类来调用父类和子类同名的方式时,调用的究竟是哪个子类或父类的方法,由运行时确定,这被称为动态绑定。
多态和动态绑定是计算机程序的一种重要的思维方式,使得操作对象的程序不需要关注对象的实际类型,从而可以统一处理不同的对象,但又能实现每个对象的特有行为。
4.2 继承的细节
在子类创建的过程中,父类的构造方法必须被调用。此时,子类可以通过super
调用父类的构造方法,如果子类没有通过super
调用,则编译器会自动调用父类的默认构造方法,如果父类没有默认构造方法,则会出现编译错误。
如果在父类的方法中调用了可以被子类重写的方法,那么可能会出现意想不到的结果。因为子类如果调用了父类的某个方法A,并且A会调用已经被子类重写的方法B。即使A是父类的方法,但A调用的B不再是父类的方法,而是被重写的方法。而在构造方法中出现这种情况是一种不好的实践,容易引起混淆,因此构造方法应该只调用private
方法。
子类可以重写父类的非private
的方法,当调用时会动态绑定,但对于实例变量、静态方法、静态变量和private方法,如果子类和父类的变量和方法重名了,那么在调用时执行的是静态绑定。也就是说,如果通过父类变量来调用,则调用父类的方法和属性,如果通过子类变量来调用,则调用子类的方法和属性。而对于实例方法的动态绑定,我们可以通过一个父类变量来调用子类的实例方法,至于是哪个方法,则可以在运行时动态确定。
和重写相似的是重载。重载只是方法名称相同而函数签名(参数个数、类型或顺序)不同,而重写要求除方法定义外的一切完全相同。当有多个重载函数时,会根据调用时的参数在所有的重载版本中选择出一个最匹配的版本来调用。然后再查看变量的动态类型,进行动态绑定。
我们可以将一个子类变量赋值给一个父类变量,但父类变量是否能赋值给一个子类变量取决于父类变量的动态类型。也就是说,只有当这个父类变量引用的变量类型确实是将要赋值的子类变量时才会成功,否则会发生编译错误。可以通过instanceof
关键字来检查一个父类变量是否引用的是一个子类对象。
public boolean canCast(Base b) {
// child是子类
return b instanceof Child;
}
protected
关键字表示属性或方法虽然不能被外部访问,但可以被子类访问或者是被同一个包内的其他类访问,不管其他类是否是该类的子类。proteced
的一种常见的应用场景是模板方法。
重写方法时,子类方法可以提高但不能降低父类方法的可见性。如果父类方法是public
,那么子类方法也需要是public
;如果父类方法是protected
,那么子类方法可以是protected
,也可以是public
。
可以使用修饰符final
来防止继承。如果final
修饰的是一个类,那么这个类不可以被继承。如果final
修饰的是类的方法,那么这个方法在不可以被子类重写。
4.3 继承的基本原理
在Java中,所谓类的加载是将类的相关信息加载到内存。在Java中,类是动态加载的,当第一次使用这个类的时候才会加载,加载一个类时,会查看其父类是否已加载,如果没有,则会加载其父类。
在创建对象时,其过程如下
- 分配内存。
- 执行静态变量和静态初始化代码。
- 对所有实例变量赋默认值。
- 执行实例初始化代码。
分配的内存包括本类和所有父类的实例变量,但不包括任何静态变量。实例初始化代码的执行先从父类开始,然后再执行子类的初始化代码。再执行任何初始化代码前,所有的实例变量已经设置完默认值。
寻找要执行的实例方法时先从对象的实际类型信息开始查找,找不到时再查找父类的类型信息。如果继承的层次比较深,要调用的方法位于比较上层的父类,则需要经过多次查找,大多数系统则使用一种称为虚方法表的方法来优化调用的效率。所谓虚方法表就是在类加载的时候为每个类创建一个表,记录该类的对象所有动态绑定的方法及其地址,但一个方法只有一条记录。子类重写了父类方法后只会保留子类的。
对变量的访问是静态绑定的。
4.4 继承是把双刃剑
继承是具有破坏力的,主要存在以下问题。
- 继承破坏封装,因为子类和父类之间可能存在细节的依赖。
- 继承并没有反映
is-a
关系。