8.2 继承
在面向对象的程序设计中,继承是一个不可分割的重要组成部分,没有使用继承的程序设计,就不能称为面向对象的程序设计。继承的重要性和特殊性可以通过本章的学习得以领会。
8.2.1 继承的概念
在前面的课程中,根据《租车系统》的需求抽象出了Car类和Truck类,在这两个类中有许多相同的属性和方法,例如name、oil、loss属性以及相应的getter方法,还有addOil( )和drive( )方法。这样做有两个问题,一是代码大量重复,二是如果要修改,两个类都要修改,会很麻烦,而且容易忘记修改部分内容。怎么解决这个问题呢?使用继承解决这个问题。
Java继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的属性或新的方法,也可以用已存在的类的属性和方法。这种技术能够非常容易地复用以前的代码,大大缩短开发周期,降低开发费用。
了解Java继承的概念后,接下来使用继承来解决Car类和Truck类重复代码的问题。作为程序设计人员,可以将Car类和Truck类重复的代码挑出来,提取到一个单独的Vehicle类中,然后让Car类和Truck类继承Vehicle类,这样可以在保留Vehicle类的属性和方法的同时,增加不同的属性和方法。继承的类图如图8.9所示。
继承的语法形式如下。
class A extends B{
类定义部分
}
即A类继承B类,B类称为父类、超类或基类,A类称为子类、衍生类或导出类。
8.2.2 继承的使用
根据图8.9编写Vehicle类,Car类和Truck类,和类图略有不同的是,在Vehicle类中,增加了两个构造方法,一个是有参的,另一个是无参的,Vehicle类的代码如下。
package com.bd.zuche;
//车类,是父类
public class Vehicle
{
private String name = "汽车"; //车名
private int oil = 20; //油量
private int loss = 0; //车损度
//无参构造方法
public Vehicle()
{
}
//构造方法,指定车名
public Vehicle(String name)
{
this.name = name;
}
//获取车名
public String getName()
{
return name;
}
//获取油量
public int getOil()
{
return oil;
}
//获取车损度
public int getLoss()
{
return loss;
}
//加油
public void addOil()
{
if(oil > 40) //如果加油20升则超过油箱容量,则加到60升即可
{
oil = 60;
System.out.println("邮箱已加满!");
}else{ //加油20升
oil = oil + 20;
}
System.out.println("加油完成!");
}
//行驶
public void drive()
{
if(oil < 10)
{
System.out.println("油量不足10升,需要加油!");
}else{
System.out.println("正在行驶!");
oil = oil - 5;
loss = loss + 10;
}
}
}
下面是Car类的定义,和类图不同的也是增加了一个有参的构造方法。
package com.bd.zuche;
//轿车类,是子类,继承Vehicle类
public class Car extends Vehicle
{
private String brand = "红旗"; //品牌
//构造方法,指定车名和品牌
public Car(String name,String brand){
super(name); //使用super关键字,调用父类的构造方法
this.brand = brand;
}
//显示车辆信息
public void show()
{
System.out.println("显示车辆信息:\n车辆名称为:" + this.name + " 品牌是:" + this.brand + "油量是:" + this.oil + " 车损度为:" + this.loss);
}
//获取品牌
public String getBrand()
{
return brand;
}
}
需要注意的是,在Car类的构造方法中,有 super(name);这条语句,其含义为调用父类有参的构造方法。
前面已经学过,在一个类中,this关键字代表这个类对象本身。与this关键字类似,super关键字代表当前对象的直接父类对象的默认引用,在子类中可以通过super关键字来访问父类的成员。
编译上面代码,编译器报错,如图8.10所示。
这里暂且不讨论报错的原因,这是下一小节介绍的重点,只需要把Vehicle类私有的成员变量name、oil和loss改成默认类型的,编译即可通过。
下面是Truck类的定义。
package com.bd.zuche;
//卡车类,是子类,继承Vehicle类
public class Truck extends Vehicle
{
private String load = "10吨"; //吨位
//构造方法,指定车名和吨位
public Truck(String name,String load){
super(name); //使用super关键字,调用父类的构造方法
this.load = load;
}
//显示车辆信息
public void show()
{
System.out.println("显示车辆信息:\n车辆名称为:" + this.name + " 吨位是:" + this.load + "油量是:" + this.oil + " 车损度为:" + this.loss);
}
//获取品牌
public String getBrand()
{
return load;
}
}
运行8.1节的TestZuChe类,运行结果如图8.11所示。
修改8.1节TestZuChe2类的代码,将原先创建的Car类对象替换成Truck类对象,代码如下,运行结果如图8.12所示。
import java.util.Scanner;
import com.bd.zuche.*;
class TestZuChe2
{
public static void main(String[] args)
{
Truck truck = new Truck("大力士二代","10吨");//初始化卡车对象truck
truck.show(); //输出车辆信息
truck.drive(); //让truck行驶1次,油量剩下15升,车损度为10
truck.show(); //输出车辆信息
truck.drive(); //让truck再行驶1次,油量剩下10升,车损度为20
truck.drive(); //让truck再行驶1次,油量剩下5升,车损度为30
truck.drive(); //让truck再行驶1次,因油量不足10升,不行驶,提示需要加油
truck.addOil(); //让truck加油1次,油量增加20升,达到25升
truck.show(); //输出车辆信息
}
}
8.2.3 继承和访问权限
前面在《租车系统》中,Car类和Truck类继承自Vehicle类,通过它们介绍了如何使用继承。接下来,需要了解继承和访问权限之间的关系。
继承最大的好处是,子类可以从父类中继承属性和方法,那么子类是不是能继承父类所有的属性和方法呢?具体情况说明如下。
子类可以继承父类中访问权限修饰符为public和protected的属性和方法。
子类可以继承父类中用默认访问权限修饰的属性和方法,但子类和父类必须在同一个包中。
子类无法继承父类中访问权限修饰符为private的属性和方法。
子类无法继承父类的构造方法。
回忆一下,刚才用Car类继承Vehicle类,Vehicle类中name、oil和loss都是私有属性,Car类在继承Vehicle类时,是不能继承这些属性的,所以在Car类show()方法的代码中出现访问name、oil和loss属性时,编译器就会报错。之后,通过将name、oil和loss这三个属性的访问权限修饰符从private调整为default,且Car类和Vehicle类在同一个包中,所以Car类继承了Vehicle类默认的属性name、oil和loss,解决了这个问题。
针对这样的情况,还有一种解决方式是将show()方法里直接访问name、oil和loss属性的代码修改为这些属性对应公有的getter方法的代码即可,修改完的代码如下所示。
System.out.println("显示车辆信息:\n车辆名称为:" + getName() + " 品牌是:" + this.brand + "油量是:" + getOil() + " 车损度为:" + getLoss());
构造方法是一种特殊的方法,子类无法继承父类的构造方法。那么在子类的构造方法中,尤其要注意,子类构造方法中如果没有显式调用父类有参构造方法(例如super(name);),没有通过this显式调用自身的其他构造方法,则系统会默认调用父类无参构造方法(super();)。
8.2.4 方法重写
子类可以从父类继承相应访问权限的方法,但如果父类的方法不能满足子类的需要,则可以在子类中对父类的同名方法进行覆盖,这就是重写。
假设《租车系统》中,系统的需求发生了如下变化。
卡车每行驶1次,耗油从5升提升为10升,增加车损度10,如果油量低于15升,则不允许行驶,直接在控制台输出“油量不足15升,需要加油!”
在Truck类中添加如下的代码,重写父类的drive()方法。
//子类重写父类的drive( )方法
public void drive()
{
if(oil < 15)
{
System.out.println("油量不足15升,需要加油!");
}else{
System.out.println("正在行驶!");
oil = oil - 10;
loss = loss + 10;
}
}
使用下面的代码进行测试,注意看测试代码的注释,运行结果如图8.13所示。
import java.util.Scanner;
import com.bd.zuche.*;
class TestZuChe3
{
public static void main(String[] args)
{
Truck truck = new Truck("大力士二代","10吨");//初始化卡车对象truck
truck.show(); //输出车辆信息
truck.drive(); //让truck行驶1次,油量剩下10升,车损度为10
truck.show(); //输出车辆信息
truck.drive(); //让truck再行驶1次,因油量不足15升,不行驶,提示需要加油
truck.drive(); //让truck再行驶1次,因油量不足15升,不行驶,提示需要加油
truck.drive(); //让truck再行驶1次,因油量不足15升,不行驶,提示需要加油
truck.addOil(); //让truck加油1次,油量增加20升,达到30升
truck.show(); //输出车辆信息
}
}
在上面的例子中,子类Truck完全重写了父类Vehicle的drive()方法。还有一种在方法重写的过程中经常遇到的情况是,子类并不需要全部重写父类的方法,而只是需要在父类方法的基础上增加一些功能,这样可以在子类重写的方法中编写“super.父类方法名();”的代码,调用父类被重写的方法。
另外,重写需要满足如下条件。
重写方法与被重写方法同名,参数列表也必须相同。
重写方法的返回值类型必须和被重写方法的返回值类型相同或是其子类。
重写方法不能缩小被重写方法的访问权限。
在前面的章节中,学过final关键字,用final修饰的变量即为常量,只能赋值一次。如果用final修饰方法,则该方法不能被子类重写。用final修饰类,则这个类不能被继承。
8.2.5 属性覆盖
8.2.4节讲到,子类可以重写父类的方法,完成子类特定的功能。如果子类覆盖父类的属性,会有什么样的结果呢?
public class Sub extends Super{
public int i = 100 ; //子类同名属性i,赋值100
public static void main(String[] args) {
Sub sub = new Sub(); //创建子类对象
System.out.println(sub.i); //输出子类对象的i属性
}
}
class Super {
public int i = 50 ; //父类属性i,赋值50
}
程序运行的结果是100,说明子类的属性(值为100)覆盖了父类的属性(值为50)。将代码修改为如下内容。
public class Sub extends Super{
public int i = 100 ; //子类同名属性i,赋值100
public static void main(String[] args) {
Super sup = new Sub(); //创建父类对象,用子类实现
System.out.println(sup.i); //输出sup的i属性
}
}
class Super {
public int i = 50 ; //父类属性i,赋值50
}
程序运行结果是50,说明创建父类对象,实现的时候用子类实现,此时这个对象的属性为父类的属性,不被子类覆盖。
如果创建的是父类对象,实现的时候用子类实现,再调用这个对象的方法(子类重写了父类的该方法),其结果又如何呢?父类的方法会被子类方法覆盖吗?请看下面的代码。
public class Sub extends Super{
public int i = 100;
public void show() //子类方法重写,显示“子类方法”
{
System.out.println("子类方法");
}
public static void main(String[] args) {
Super sup = new Sub(); //创建父类对象,用子类实现
sup.show(); //调用的是子类方法,覆盖了父类方法
System.out.println(sup.i);
}
}
class Super {
public int i = 50;
public void show() //父类方法,显示“父类方法”
{
System.out.println("父类方法");
}
}
程序输出为“子类方法”和50。通过运行结果可以说明,父类的方法被子类覆盖,调用了子类重写的方法,显示出“子类方法”。
8.2.6 继承中的初始化
前面的章节已经介绍了对象初始化过程,不过那时候还没有学习继承的概念。接下来通过一个案例,分析在继承的条件下,父类、子类中的静态块、非静态块、构造方法的执行顺序,看下面的代码。
public class InitDemo {
public static void main(String[] args) {
System.out.println("第一次实例化子类:");
new Sub();
System.out.println("第二次实例化子类:");
new Sub();
}
}
class Super {
static {
System.out.println("显示:父类中的静态块!");
}
{
System.out.println("显示:父类中的非静态块!");
}
Super() {
System.out.println("显示:父类构造方法!");
}
}
class Sub extends Super {
static {
System.out.println("显示:子类中的静态块!");
}
{
System.out.println("显示:子类中的非静态块!");
}
Sub() {
System.out.println("显示:子类构造方法!");
}
}
程序运行结果如图8.14所示。
通过运行结果可以看出,在第一次实例化子类时,先调用父类的静态块,再调用子类的静态块,之后再调用父类的非静态块和构造方法,再调用子类的非静态块和构造方法。注意,当第二次实例化子类时,父类和子类的静态块都不会再被调用,因为它们是静态块,属于类级别的,只会被调用一次。