第五章 继承
继承是指基于已有的类构造一个新类,继承已有类就是复用(继承)这些类的成员变量和方法。并在此基础上,添加新的成员变量和方法,以满足新的需求。java不支持多继承。
5.1 类、超类和子类
5.1.1 定义子类
下面是由继承Employee类来定义Manager类的格式,关键字extend
表示继承。
public class Manager extend Employee{
//添加方法和成员变量
}
extend
表明定义的新类派生于一个已有类。被继承的类称为超类、基类或父类;新类称为子类或派生类。
在Manager类中增加了一个用于存储奖金信息的域,以及一个用于设置这个域的新方法:
public class Manager extend Employee{
private double bonus;
...
public void setBonus(double bonus){
this.bonus = bonus;
}
}
尽管在Manager类中没有显式的定义getName
和getHireDay
等方法,但Manager类的对象却可以使用它们,这是因为Manager类自动继承了超类Employee中的这些方法。此外,Manager类还从父类继承了name、salary
和hireDay
这3个成员变量。从而每一个Manager对象包含四个成员变量。
5.1.2 覆盖方法(Override)
子类除了可以定义新方法外,还可以覆盖(重写)父类的方法。覆盖要遵循 "两同两小一大"的原则:
两同指:
- 方法名相同。
- 形参列表相同,即形参个数相同,各形参类型必须对应相同(新方法的形参类型不可以是父类方法形参类型的子类型)。
两小指:
- 子类方法返回值类型应与父类方法返回值类型相同或是父类方法返回值类型的子类型。
- 子类方法声明抛出的异常应与父类方法声明抛出的异常类型相同或是其子异常。
一大指:子类方法的访问权限应与父类方法的访问权限相同或比父类访问权限大。
从此处可以看出,覆盖和重载的区别:
方法重载对返回值类型和修饰符没有要求;实例方法可以重载父类的类方法,而方法覆盖要求返回值类型变小,修饰符变大且只能为实例方法,满足上述条件的静态方法,不叫覆盖,只是屏蔽。
尤其要指出的是:满足覆盖条件的方法要么都是类方法,要么都是实例方法,不能一个是类方法,一个是实例方法,否则会引发编译错误。
编译时,编译器会为每个类建立一张方法表,表中包含的方法有:子类中定义的所有方法(包括私有方法,构造器,类方法和公有方法)。同时包含从父类继承的非私有方法,包含重载的方法,应该也包含类方法,但不包含被子类覆盖的方法。此外,编译器也会为每个对象分配一个成员变量表,其中包含本类中定义的所有变量(成员变量和类变量),以及父类中非私有的变量。
当子类覆盖了父类方法后,子类对象将无法访问父类中被覆盖的方法,但子类方法中仍能调用父类中被覆盖的方法,调用格式为:
- "super.方法名" (被覆盖的是实例方法),仅可以在子类非静态方法使用。编译时,编译器会根据super去查找父类的方法表,并替换为匹配方法的符号引用。
- "父类类名.方法名"
public class Manager extend Employee{
private double bonus;
...
public double getSalary(){
return super.getSalary() + bonus;
}
}
如果父类方法的访问权限是private
,则子类无法访问该方法,且无法覆盖该方法,当子类中定义了一个与父类 private
方法满足上述”两同两小一大“原则的方法时,并没有覆盖该方法,而是在子类中定义了一个新方法。例如:
class BaseClass{
//test()方法是private访问权限,子类不可访问
private void test(){...}
}
class SubClass extends BaseClass{
//此处不是覆盖,所以可以增加static关键字
public static void test(){...}
}
从而,子类可以继承的内容有
- 父类的成员变量(对于父类的私有成员变量,子类对象依然分配有内存,仅因为子类对象不能直接访问)。
- 父类的非私有方法。
不能继承的有:
- 父类中被覆盖的方法,不能继承。
- 父类的私有方法。
5.1.3 super
限定
super用于限定该对象调用它从父类继承得到的实例变量或方法。super不能出现在static
修饰的方法中。
如果在构造器中使用super
,则super
用于初始化从父类继承的成员变量。
对于非私有域而言,如果子类定义了和父类同名的实例变量,则会发生子类实例变量隐藏父类实例变量的情形。默认情况下,子类里定义的方法直接访问的同名实例变量是子类的实例变量。但可以在使用"super.实例变量名"来访问被隐藏的父类的实例变量。例如:
class BaseClass{
public int a;
}
class SubClass extends BaseClass{
public int a = 7;
public void test1(){
System.out.println(a);
}
public void test2(){
System.out.println(super.a);
}
}
如果子类中没有与父类同名的成员变量,那么在子类实例方法中访问该成员变量时,无须显式使用super
或父类名。如果某个方法中访问了名为a
的实例变量,但没有显式指定调用者,则系统查找a
的顺序为:
- 查找该方法中是否有名为
a
的局部变量 - 查找当前类是否有名为
a
的成员变量 - 查找直接父类中是否包含了名为
a
的成员变量,依次上溯所有父类,直至Object
类,如果没有找到,则报错。
当程序创建一个子类对象时,系统不仅会为子类中定义的实例变量分配内存,也会为从父类继承的所有实例变量分配内存,即使子类中定义了与父类中同名的实例变量。
由于子类的实例变量仅是隐藏了父类中同名的实例变量,不是覆盖。所以,访问哪个实例变量是由调用者的类型决定的。在编译时,由编译器分派。从而,会出现如下情形:
class Parent{
String tag = "parent";
}
class Child extends Parent{
private String tag = "child";
}
public class Test{
public static void main(String[] args){
Child c = new Child();
//报错,不可访问私有变量
out.println(c.tag);///////1
//输出:parent
out.println(((Parent)c).tag);//////2
}
}
当程序在代码1处试图访问tag
时,由于调用者为子类,而子类的tag
是私有变量,不能在外部被访问.。而代码2处访问的是父类的tag
。此与方法的覆盖不同,方法覆盖具有多态性。
综上,当子类中隐藏了父类的实例变量,或子类中覆盖了父类的方法时,父类被隐藏的实例变量,或被覆盖的方法,仅能在子类的方法里面通过super
来访问,在其他类的方法中无法访问。
5.1.4 子类构造器
由于子类不能访问父类的私有域,所以需要利用父类的构造器来初始化这部分私有域,可以通过super实现对父类构造器的调用。使用super调用构造器的语句必须是子类构造器的第一条语句。
不管是否使用super
显式的调用了父类构造器,子类构造器总会调用一次父类构造器,子类调用父类构造器分如下几种情况:
- 子类构造器执行体的第一行使用
super
显式调用了父类构造器,系统将根据super
调用传入的实参列表调用父类构造器。 - 子类构造器执行体的第一行代码使用
this
显式调用本类中重载的其他构造器,系统将根据this
调用里传入的实参列表调用本类的另一个构造器。执行本类中另一个构造器时即会调用父类构造器。 - 子类构造器执行体中既没有使用
super
调用,也没有使用this
调用,系统将会在执行子类构造器之前,隐式调用父类的无参数构造器。(所以,定义类时,最好提供一个无参数构造器)。
综上,调用子类构造器时,父类构造器总会在子类构造器之前执行。以此类推,执行父类构造器时,系统会再次上溯执行其父类的构造器.....从而,创建任何java
对象,最先执行的总是Object
类的构造器。当父类有构造器,但不存在默认构造器时,程序出错。
实际上初始化块是一个假象,源文件编译时,初始化块中的代码会被"还原"到每个构造器中,且位于构造器所有代码的前面。(super()调用的后面,this调用的前面??)。
在创建子类对象时,各模块的初始化流程如下:
- 在未执行任何初始化语句时,系统已经为子类的所有成员变量,以及父类的所有成员变量分配了内存空间,并默认初始化,当子类中存在成员变量隐藏父类成员变量情况时,这两个成员变量都会被分配内存。
- 然后初始化父类(先执行初始化语句和初始化块,再执行父类构造器,若子类构造器中有
super
调用父类构造器语句,则调用指定的父类构造器,否则调用父类的默认构造器。 - 按序执行子类的显式初始化语句或初始化块。
- 执行子类构造器内的语句。
示例如下:
public class Test{
{
a = 6;
}
int a = 9;
public static void main(String[] args){
//输出为9;若调换两个初始化语句,输出为6
out.println(a);
}
}
在首次使用子类时,类的初始化流程为:
若父类未初始化,则执行父类的类初始化,在执行父类初始化时,也要先判断其父类是否已初始化,若未初始化,则先执行其父类的初始化,……直至Object类,最后才执行本类的类初始化。
5.2 多态
Java
中引用变量有两个类型:编译时类型和运行时类型。编译时类型由变量定义的类型决定,运行时类型由该变量引用对象的类型决定。如果编译时类型和运行时类型不一致,就可能出现多态(PolyMophism
)。
因为子类是一种特殊的父类,所以Java
允许把一个子类对象直接赋给一个父类变量,由系统自动完成,无须类型转换,称为向上转型。但不能将父类对象直接赋给子类变量。
两个同类型的变量,若一个引用父类对象,另一个引用的是子类对象,且子类中覆盖了父类的方法,那么两个变量同时调用此方法时,将呈现出不同的行为特征,这被称为多态。
当父类变量引用子类对象时,由于变量的编译时类型为父类,所以只能调用父类的方法和成员变量,以及子类中覆盖的方法(动态连接、动态绑定),不能调用子类中新定义的、以及父类中存在,但子类重载后的只属于子类方法。符号引用在编译时分派。
与方法不同的是,对象的实例变量不具备多态性。总是访问编译时类型中定义的实例变量。
在继承链中对象方法的调用存在一个优先级:????
this.show(O),super.show(O),this.show((super)O),super.show((super)O)
警告,在java
中,子类数组的引用可以赋给父类数组的引用,而不需要强制类型转换。例如:
Manager[] managers = new Manager[10];
//将它赋给Employee[] 数组是完全合法的:
Employee[] staff = managers;
因为managers[i]是一个Manager,可以赋给Employee变量,但是编译器不允许让数组元素再引用其他类型的对象,不允许数组元素引用的类型不一致。如下面语句将会抛出ArrayStoreException
异常:
staff[0] = new Employee(...);
因为如果允许staff[0] 和 managers[0] 都引用了这个Employee 对象,那么managers[0].getBonus()
变的不合理。
即数组元素只能引用相同类型的对象,否则编译器会报告异常。
理解初始化流程和多态性的一个极好的例子:
public class Mytest {
public static void main(String[] args){
Dervied td = new Dervied();
td.test();
}
}
class Dervied extends Base {
private String name = "dervied";
public Dervied() {
super();
tellName();
printName();
}
public void tellName() {
System.out.println("Dervied tell name: " + name);
}
public void printName() {
System.out.println("Dervied print name: " + name);
}
}
class Base {
private String name = "base";
public Base() {
tellName();
printName();
}
public void tellName() {
System.out.println("Base tell name: " + name);
}
public void printName() {
System.out.println("Base print name: " + name);
}
public void test(){
tellName();
}
}
输出结果为:
//前两句输出,说明Base类中的tellName()和printName()仍然调用的是Dervied类的方法
//同时也说明,初始化流程为:先调用父构造器,而不是先执行显式初始化语句:private String name = "dervied";
Dervied tell name: null
Dervied print name: null
//三、四两句说明调用完Base类构造器后,初始化流程为:先执行显式初始化语句,然后再执行构造器中的语句。
Dervied tell name: dervied
Dervied print name: dervied
//最后一句再次验证了,Base类中的tellName()和printName()已经彻底被覆盖
Dervied tell name: dervied
5.3 理解方法调用
假设要调用x.f(args)
,下面是调用过程的详细描述:
-
编译器查看对象的编译时类型类型和方法名。假设隐式参数 x 的编译时类型为 C 类。需要注意的是:有可能存在多个名字为 f,但是形参列表不同的方法。例如可能存在方法
f(int)
和方法f(String)
。编译器从方法表中列举出所有方法名为f
的方法,包括父类中非私有的且名为 f 的方法。至此,编译器已获得所有可能被调用的候选方法。 -
接下来,编译器将查看调用方法时提供的实参类型。如果在所有名为 f 的方法中存在一个与提供的参数类型完全匹配,就选择这个方法。这个过程被称为重载解析(overloading resolution)。由于允许类型转换(int 可以转换成 double,子类转换成父类,等等),所以过程很复杂,如果编译器没有找到与参数类型匹配的方法,或者发现经过类型转换后有多个方法与之匹配,就会报错。至此,编译器已确定需要调用的方法,会将其替换为对应方法的符号引用。
5.4 阻止继承:final 类和方法
不允许继承的类被称为 final 类。在定义类时使用 final 修饰符,就表明这个类是 final 类,不能被继承,声明格式如下:
public final class Executive{
...
}
类中特定的方法也可以被声明为 final。被 final 修饰的非私有方法不能被子类覆盖,但是方法仍然可以被重载(因为重载不考虑修饰符)。子类中可以定义父类中被final
修饰的私有方法。
域也可以被声明为 final,final 域表明域为常量。当一个类被声明为 final 时,只是其中的方法变为 final,不包括域。
5.5 强制类型转换
引用变量只能调用编译时类型中的方法,不能调用运行时类型中定义的方法。如果需要调用运行时类型中的方法,必须进行类型转换,将它强制转换成运行时类型。引用类型转换的语法和基本类型的强制转换相同。但是,如果父类变量实际类型是超类时,强制类型转换会引发ClassCastException
。因此,可以在进行类型转换之前,先使用instanceof
检测是否能进行转换:
if(staff[1] instanceof Manager){
boss = = (Manager) staff[1];
...
}
如果检测返回false,编译器就不会进行转换。
综上所述:
- 只能在继承层次内进行类型转换。
- 在将超类转换成子类之前,应该使用
instanceof
进行检查。
只有在使用子类特有的方法时才需要进行类型转换。建议尽量少用到类型转换和 instaceof
运算符。
5.6 instanceOf
运算符
instanceOf
运算符的前一个操作符通常是一个引用类型变量,后一个操作符是一个类或接口;instanceOf
用于判断前面引用变量的运行时类型是否是后面类或者其子类的实例。如果是,则返回true
,否则返回false
。
使用
instanceOf
运算符时,要求前面的操作数的编译时类型与后面操作数的类型相同,或者前者是后者的父类,或者前者是后者的子类。否则会引起编译错误。
5.7 继承与组合
5.7.1 使用继承的注意点
允许子类继承父类,子类可以直接访问父类的成员变量和方法。但是继承破坏了父类的封装性:子类可以通过覆盖的方式改变父类方法的实现,从而导致子类可以恶意篡改父类的方法。并且,父类方法调用被覆盖方法时,调用的其实是子类的方法。
为了保证父类良好的封装性,设计父类通常应该遵循如下规则:
- 尽量隐藏父类的内部数据,尽量把父类的所有成员变量设置为
private
。 - 不要让子类可以随意访问、修改父类的方法。父类中作为辅助的方法应设置为
private
。父类中需要被外部类调用的方法,必须设置为public
,如果不希望子类重写该方法,可以用final
修饰;如果希望父类的某个方法被子类重写,但不希望被其他类自由访问,则可以使用protected
来修饰。 - 尽量不要在父类构造器中调用将被子类重写的方法,容易出现逻辑混乱。
5.7.2 组合
组合是将待复用的类当成另一个类的一部分,即在新类中,定义一个待复用类的私有成员变量,以实现对类方法和成员变量的复用。例如:
class Animal{
public void breath(){
System.out.println("吸气,吐气。。。");
}
}
class Bird{
private Animal a;
public void breath(){
a.breath();
}
}
组合和继承都可以复用指定类的方法以及成员变量。继承更符合现实意义。组合可以避免破坏父类的封装性。
5.8 受保护的访问
如果希望超类中的某些方法允许被子类访问,或允许子类的方法访问超类的某个域,此时,需要将这些方法或域声明为 protected
。
但是,将父类的域声明为 protected后,子类的方法只能访问子类对象的 protected 域,不能访问父类对象的 protected 域。这种限制有助于避免滥用受保护机制,使得子类只能获得受保护域的权利。
归纳 java
用于修饰成员变量或方法的控制可见性的4个访问修饰符:
- 仅对本类可见 -- private
- 对所有类可见 -- public
- 对本包和所有子类可见 -- protected
- 仅对本包可见 -- 默认,不添加修饰符
类的可见性:
- 对所有类可见 --
public
- 仅对本包可见 -- 默认,无修饰符。
- 对于内部类而言,本包和子类可见 -- protected
- 对于内部类而言,本类可见 --
private