3.3 继承
在面向对象程序设计中,可以从已有的类派生出新类,这种机制称为继承(Inheritance)。继承是Java语言在软件重用方面一个重要且功能强大的特征。通过继承,可以由一个类派生出一个新类,这个新类不仅拥有原来类的数据成员和成员方法,并且可以增加自己的数据成员和成员方法。
3.3.1 概述
继承性是面向对象程序设计思想的一个重要特征,它可使代码重用,避免不必要的重复设计,降低程序的复杂性。被继承的类称为超类(Super Class)或父类(Parent Class),派生出来的新类称为子类(Sub class)。Java语言继承性支持单重继承,即一个超类可以派生出多个子类,子类还可以派生出新的子类,但一个子类只能有一个直接超类。Java语言不支持多重继承。从图3-2中可以看出,Java语言的单重继承机制又可以称为单根继承。现实世界中存在许多继承类的抽象实例,如在某公司工作的经理,其待遇和普通员工的待遇存在一定的差异,不过他们之间也存在着很多相同的地方,比如他们都领取薪水,只是普通员工在完成本职工作后仅仅领取薪水,而经理在完成预期的业绩之后还能得到奖金。将这个具体问题抽象成代码,可以封装一个员工类Employee,再封装一个经理类Manager,此时可以利用继承让Manager类作为Employee类的子类,Manager类不仅拥有Employee类的所有代码而且还可以增加自己独有的代码。因此,在子类Manager类和超类Employee类之间存在着明显的is-a关系,每一名经理都是一名员工,is-a关系是判断继承的一个明显标志。
Java语言继承的一般书写格式为:
例如:
说明:
(1)子类可以继承其超类的代码和数据,即在子类中可以直接执行超类的方法和访问超类的数据。
(2)在子类中可以增加超类中没有的数据和方法。
(3)在子类中可以重新定义超类中已有的成员变量,即在子类中允许定义与超类同名的成员变量。
(4)在子类中可以重载超类中已有的方法,包括构造方法和成员方法。
(5)在子类中可以重新定义超类中已有的成员方法,实现同名方法的不同执行结果。
(6)在类的声明中如果没有显式地写出extends关键字,这个类被Java系统默认为是Object的子类。在Java语言中,Object类是所有类的根类,是最终超类,它是java.lang包中的类。从Object类派生出若干子类,形成了Java平台的类层次结构,通过Java SE的API规范可以查询Java语言提供的每个类的继承或被继承关系。
例题3.13 封装了继承关系的程序。
1.继承中成员变量的隐藏
Java语言继承中,子类可以继承超类所有非私有的成员变量,可以增加子类自己的成员变量。有时,被继承的超类的成员变量可能在子类中出现名字相同但性质不同的情况,这就需要在子类中对从超类继承而来的成员变量进行重新定义,称为变量的隐藏(Hidden)。当需要隐藏超类中的成员变量时,必须在子类中定义与超类同名的成员变量。这时,子类对象拥有了两个名字相同的变量(声明的类型可以不同),一个是继承自超类,另一个由自己定义。当子类对象执行继承自超类的方法时,处理的是继承自超类的成员变量。而当子类对象执行它自己定义的方法时,处理的则是它自己定义的变量,而把来自超类的变量隐藏起来。
例题3.14 封装了隐藏成员变量的程序。
子类一旦隐藏了继承的非私有成员变量,那么子类对象就不再拥有该变量。要想使用该变量,可以让子类对象调用超类的方法对其操作,也可以在子类的成员方法中使用super关键字进行操作。
例题3.15 封装了用super关键字操作被隐藏变量的程序。
2.继承中构造方法的调用
由于构造方法的特殊性,Java语言继承中构造方法是不能被继承的。但是当用子类的构造方法创建一个子类对象时,子类的构造方法总是默认在第一条语句处用super关键字调用超类的构造方法。也就是说,如果子类的构造方法没有显式地指明调用超类的哪个构造方法,子类就调用超类的不带参数的构造方法,即,如果在子类的构造方法中省略了super关键字来调用某个构造方法,那么默认格式为super();,且该语句必须位于子类构造方法的第一条语句处。
前面已经提到,如果一个类里定义了一个或多个构造方法,那么默认的无参数的构造方法就自动消失,因此,当在超类中定义多个构造方法时,这多个构造方法里一定要包含一个无参数的构造方法,以避免子类省略super关键字时出现编译错误。
例题3.16 封装了构造方法在继承中super关键字的程序。
3.继承中成员方法的重载
在一个类中定义多个方法名相同而参数不同的方法,称为方法的重载(Overload)。Java语言继承中,超类的数据成员和成员方法相当于是子类自己的数据成员和成员方法,如果在超类中定义了一个方法或多个方法,那么在子类中就可以对这些方法进行重载编写,就像在一个类中进行重载一样,从而实现Java编译时的多态特征。
4.继承中成员方法的重写
Java语言继承中,如同子类可以定义与超类同名的成员变量,实现对超类成员变量的隐藏一样,子类也可以重新定义与超类同名且同参数的实例方法,以实现对超类方法的重写(Override),也可以称为方法覆盖。如果子类可以继承超类的某个实例方法,子类就可以重写这个方法。重写方法是指在子类中定义一个方法,该方法的返回类型和超类中同名方法的返回类型一致或者是超类方法返回类型的子类型(所谓子类型是指超类同名方法的返回类型是引用类型,允许子类重写的方法的返回类型可以是其子类),并且这个同名方法的参数个数、参数类型和超类的方法完全相同,但是方法体的实现内容不同。这个被重写的方法不能算为子类增加的新方法。
例题3.17 封装了继承中重写方法的程序。
子类通过方法的重写机制可以隐藏继承超类的方法,把超类的状态和行为改变为子类自己的状态和行为。假如超类中有一个方法myMethod(),一旦子类重写了超类的方法myMethod(),就隐藏了继承的方法myMethod(),子类对象在调用方法myMethod()时,运行结果一定是重写了方法体的实现结果。重写方法既可以操作继承的成员变量和继承的成员方法,也可以操作子类新增加的成员变量和新增加的成员方法,但无法操作被子类隐藏的成员变量和成员方法。如果子类想操作被隐藏的成员变量和成员方法,就必须在子类的方法中调用而且同时使用关键字super。
例题3.18 使用super关键字对例题3.13进行了改进。
3.3.2 抽象类和最终类
Java语言面向对象程序设计中,无论是封装还是继承,都会涉及对成员方法的重载和重写,这两种机制都是多态特征的体现。除了前面提到的封装、继承和多态,对类的封装性还有abstract和final两种修饰符。
1.抽象类
在Java语言继承层次结构中,位于上层的类更具有通用性,甚至更加抽象,这些类封装的方法被重写的可能就更大。为了描述抽象的类封装,Java语言用关键字abstract对类进行修饰和约束,称为抽象类,它的一般书写格式为:
例如:
说明:
(1)抽象类只能被当作超类,用来被继承,不能用new来创建和实例化为对象。
(2)在抽象类中定义成员方法时,可以在方法名字前用abstract来修饰方法,这个方法称为抽象方法,如public abstract void myMethod();。抽象方法必须在子类中被重写,因此,在超类中定义抽象方法时不需要添加英文半角的花括号,只是对其声明即可。在被重写时必须给出方法体,即要有英文半角的花括号和相应的实现代码。
(3)在抽象类中也可以定义非抽象方法,这些方法就是普通的成员方法,可以被继承也可以被重写。但是,抽象方法必须被定义在抽象类中,也就是说,如果某个类中有抽象方法,这个类必须是抽象类。
例题3.19 封装了抽象类和抽象方法的程序。
2.最终类
在Java语言继承层次结构中,越位于底层的类越具有具体性,甚至更加接近于某一具体事物,这些类封装的方法几乎不可能被重写。为了描述更加具体的类封装,Java语言提出了最终类的概念,使用关键字final对类进行修饰。前面已经提到,final可以修饰Java语言常量,表示其数据量一旦被赋值就不能在其他地方被改变,按照这个意义,最终类的一般书写格式为:
例如:
说明:
(1)final类不能当作超类,不能被继承,不能有子类,只能被实例化为对象。
(2)如果认为某些封装类中的数据和方法不能被隐藏或重载或重写时,可以将其定义为final类。最常见的是Java API提供的某些常用final类,如String类等。
(3)如果用final修饰超类中的一个方法,那么这个方法就不允许被子类重写,也就是说,子类是不能隐藏超类中的final方法的,只能对其进行继承。
(4)和abstract关键字不同,final类中可以没有final方法,final方法也不是必须定义在final类中。
例题3.20 封装了final类和final方法的程序。
3.3.3 对象的引用转型
在Java语言继承机制中,单根树结构是其层次结构的体现。在单根树结构中,一个子类只能有一个直接超类,一个超类可以有多个子类,它是符合is-a关系的一种结构。与基本数据类型的自动类型转换或强制类型转换类似,这种is-a结构可以有引用的向上转型和向下转型机制。
1.对象的向上转型
在动物类的继承结构中,可以称作鱼类是动物,鸟类是动物等,这种描述是在有意强调动物的状态和行为,而忽略了鱼类独有的swim()功能或鸟类独有的fly()功能等,这种上溯类结构的方式应用到面向对象程序设计中称为对象的向上转型机制。
设Animal是Bird的超类,当用子类创建一个对象,并把子类对象的引用指向超类对象时,例如:
或:
这时,对象anAnimal称为对象aBird的上转型对象,即aBird is an Animal.。
从初始化语句可以看出,上转型对象的内存实体是由子类对象创建的,但上转型对象会丢失子类对象的一些数据或方法,对象和上转型对象的特点如图3-7所示。
图3-7 上转型对象示意图
说明:
(1)上转型对象不能操作子类新增的成员变量和成员方法(丢失了这部分属性和功能)。
(2)上转型对象可以访问子类继承或隐藏的成员变量,也可以调用子类继承或子类重写的实例方法。如果子类重写了超类的类方法(即static修饰的方法),那么子类对象的上转型对象不能调用子类重写的类方法,只能调用超类的类方法。
(3)不能把超类创建的对象引用赋值给子类对象,即不能说动物是鸟类。
(4)使用上转型对象机制的优点是体现了面向对象的多态,增强了程序的简洁性。
例题3.21 封装了上转型对象的应用程序。
程序输出的不是预期的Superclass,而是Childrenclass。这是因为smc实际上指向的是一个子类对象。Java虚拟机会自动准确地识别出究竟该调用哪个具体的方法。不过,由于向上转型,smc对象会丢失超类中没有的方法,例如shout()。可能还会这样写:
结果是一样的,而且更加有利于理解。但这样就丧失了面向对象程序设计的特点,降低了可扩展性。
例题3.22 封装了向上转型对象增强程序简洁性的应用程序。
比较Test3_22.java和Test3_221.java,可以看出,Test3_22.java有很多重复代码,而且也不易维护。有了向上转型,Test3_221.java代码可以更为简洁。
2.对象的向下转型
子类对象指向超类引用是向上转型,反过来说,超类对象指向子类引用就是向下转型。但是,向下转型可能会带来一些问题:可以说鸟类是动物,但不能说动物是鸟类。为了解决此问题,可以将上转型对象强制转换到一个子类对象,这时该子类对象又具有了子类所有的属性和功能。
例题3.23 封装了向下转型的对象应用程序。
程序运行结果如下:
之所以会出现运行时错误,是因为asc指向一个子类ASubClass的对象,所以子类ASubClass的实例对象bsc当然也可以指向asc。而sc12是一个超类对象,子类对象bsc2不能指向父类对象sc12。想要避免在执行向下转型时发生运行时ClassCastException异常,可以使用操作符instanceof。