3.3 继承

前面创建的CAuto类和CAutoFactory类已经定义了不少代码,而且由这两个类生产的SUV车型还不错。接下来,还要给SUV装上武器,用于开发军用车型。面向对象编程中,这些工作并不需要完全重新开始,而是在现有类的基础上进行改造和扩展。下面的代码(CAssaultVehicle.java文件)创建CAssaultVehicle类。

    package com.caohuayu.javademo;
public class CAssaultVehicle extends CAuto {
}

这里使用了extends关键字,其含义是扩展,但在面向对象编程概念中,更多情况下会说CAssaultVehicle类继承于CAuto类,即CAssaultVehicle类是CAuto类的子类,而CAuto类称为CAssaultVehicle类的超类(或基类、父类)。

CAssaultVehicle类中,只是让它继承了CAuto类,并没有定义任何内容。CAssaultVehiche类有什么功能呢?不如测试一下,如下面的代码所示。

    public static void main(String[] args) {
        CAssaultVehicle av = new CAssaultVehicle();
        av.model = "突击者";
        av.setDoors(5);
        av.moveTo(10, 99);
    }

图3-10 类的继承

代码执行结果如图3-10所示。

示例中,虽然CAssaultVehicle类中没有定义任何成员,但它已经从CAuto类中继承了不少东西,主要包括无参数的构造函数和非私有的成员(非private定义的成员),如model字段、setDoors()方法、getDoors()方法、moveTo()方法等。可以发现,继承的作用还是挺大的。

进一步讨论继承之前,需要注意一个问题,如果一个类不希望被继承,可以在定义时使用final关键字。下面是一个简单的示例。

    public final class C1 {
    //
    }

这样,C1类就不能被继承了,例如,下面的代码就会提示错误。

    public class C2 extends C1 {
        //
    }

另外一个需要注意的问题是,在Java中,不像在C++中那样,子类可以同时继承多个超类。也就是说,一个类同时只能有一个直接超类。

了解了这些,接下来将讨论关于继承的更多内容。

3.3.1 java.lang.Object类

定义在java.lang包的Object类有什么特殊之处?它可是Java中其他类的终极超类,也是唯一一个没有超类的类型。如果一个类没有明确指定超类,则默认继承于Object类。

对于前面创建的CAuto类、CAssaultVehicle类,以及Object类,它们的继承关系如图3-11所示。

那么,是不是在所有类中都可以使用Object类的非私有成员呢?答案是肯定的,例如,下面的代码使用了一些CAuto类中没有定义的成员。

    public static void main(String[] args) {
    CAuto auto = new CAuto();
        CAssaultVehicle av = new CAssaultVehicle();
        System.out.println(auto.toString());
        System.out.println(av.getClass().getSuperclass().toString());
    }

第一个输出语句使用toString()方法显示了auto对象的信息。第二个输出语句显示了av对象所属类型的超类信息。代码执行结果如图3-12所示。

图3-11 类继承的层次

图3-12 继承Object类成员

可以看到,在Java代码中动态处理对象和类的信息也是比较方便的,稍后还会讨论相关内容。下面先回到CAssaultVehicle类,前面提到要在车上安装武器。

3.3.2 扩展与重写

如果CAssaultVehicle类只是简单地继承CAuto类,继承的意义就不大了。实际上,在子类中可以扩展超类功能,或者对超类的功能进行重写。

首先考虑CAssaultVehicle类的构造函数。如果在子类中没有定义构造函数,默认会继承超类中的无参数构造函数;如果在子类中定义了一个构造函数,就不能直接使用超类的构造函数创建对象了。

那么,在CAuto类中创建的构造函数就无用武之地了吗?当然不是,只不过需要在CAssaultVehicle类中加个“外壳”而已。例如,下面的代码在CAssaultVehicle类中添加了一个无参数的构造函数。

代码中,使用super关键字调用超类的构造函数,分别指定型号和车门数量,这里调用的就是CAuto类中的CAuto(String m, int d)构造函数。

下面的代码测试CAssaultVehicle对象的创建。

    public static void main(String[] args) {
        CAssaultVehicle av = new CAssaultVehicle();
        av.moveTo("9号地区");
    }

图3-13 调用超类构造函数

代码执行结果如图3-13所示。

通过以上示例可以看到,创建构造函数的过程中,通过this、super关键字,可以合理地重用当前类或超类中的构造函数,使用灵活的方式来构建对象。

如果需要扩展CAssaultVehicle类的功能,直接写出来即可。下面的代码在CAssaultVehicle类中添加一个字段和一个方法。

代码中,创建了weapon字段和attack()方法。下面测试这两个新成员的使用。

    public static void main(String[] args) {
        CAssaultVehicle av = new CAssaultVehicle();
        av.weapon = "12.7mm机枪";
        av.attack("靶标");
    }

代码执行结果如图3-14所示。

此外,子类中如果需要重新实现超类中的成员,也可以直接定义。然后,还可以使用super关键字访问超类中的成员。下面的代码在CAssaultVehicle类中重写moveTo(String target)方法。

这里使用super关键字调用了超类(CAuto类)中的同名方法。下面的代码演示了新方法的使用。

    public static void main(String[] args) {
        CAssaultVehicle av = new CAssaultVehicle();
        av.moveTo("X地区");
    }

代码执行结果如图3-15所示。

图3-14 扩展类成员

图3-15 重写超类方法

3.3.3 访问级别

前面的示例中已经多次使用了访问级别的控制,这里简单总结一下Java中的常用访问级别。

□ private,定义私有成员,即成员只能在其定义的类中访问。

□ protected,受保护的成员,它可以在定义的类或子类中访问。

□ public,公共成员,它可以供类的外部代码调用。

此外,当成员不使用访问控制关键字时,称为默认(default)访问级别。默认访问级别的成员与public有些相似,可以在类的外部调用,但是默认访问级别的成员只能在其定义的包中使用。

一般情况下,出于数据的安全性,类成员的访问级别应遵循最小原则,即优先使用private级别。然后,根据需要定义为protected或public级别。对于默认访问级别,它看上去并不直观,容易让人感到困惑,所以需要熟悉其含义,并在开发中合理使用。

3.3.4 instanceof运算符

instanceof运算符用于判断一个对象是否为某个类的实例。在继承关系中,需要注意它的灵活使用。

下面的代码创建一个CAuto对象和一个CAssaultVehicle对象。

分别来看四个输出语句。

第一个输出语句中,auto对象定义为CAuto类的实例,所以显示为true,这个比较容易理解。

第二个输出语句中,av对象定义为CAssaultVehicle类的实例,但CAssaultVehiclee类定义为CAuto类的子类,所以av对象完全可以按CAuto对象的方式进行操作。

第三个输出语句中,实际上,所有对象在此都会显示为true,因为Object类是终极超类。

第四个输出语句中,auto对象不能使用CAssaultVehicle类中的新增成员,不能按CAssaultVehicle对象的方式进行工作,所以显示为false。

通过以上示例可以看到instanceof运算符的一些应用特点。

□ 所有对象与Object类的运算结果都是true。

□ 对象与其类型或其超类的运算结果为true。

3.3.5 抽象类与抽象方法

定义方法时使用abstract关键字,方法就定义为抽象方法。抽象方法并不需要包含方法体,它必须由类的子类来实现。同时,当一个类中包含抽象方法时,这个类应该定义为抽象类。

例如,下面的代码(CPlaneBase.java文件)创建一个名为CPlaneBase的抽象类。

这里,在CPlaneBase类中定义一个字段、一个构造函数和两个抽象方法,其中,抽象方法中并没有使用“{”和“}”符号定义方法体,而是直接以分号结束。

请注意,抽象类是不能创建实例的,例如,下面的代码就不能正确执行。

    CPlaneBase plane = new CPlaneBase();  // 错误

接下来,创建一个CPlaneBase类的子类,如下面的代码(CFighter.java文件)所示。

下面的代码测试CFighter类的使用。

    public static void main(String[] args) {
        CFighter f = new CFighter();
        System.out.println(f.model);
        System.out.println(f.getWeapon());
      System.out.println(f.getMaxSpeed());
    }

代码执行结果如图3-16所示。

实际应用中,抽象类更像是标准制定者,它可以定义一系列抽象方法,然后让其子类去具体实现,从而创建具有相同成员但实现各有不同的类型。

下面的代码(CConveyor.java文件)再创建一个CConveyor类,同样,它定义为CPlaneBase类的子类。

图3-16 继承抽象类

下面的代码来测试这几个类的使用。

    public static void main(String[] args) {
        CPlaneBase plane = new CFighter();
        System.out.println(plane.model);
        System.out.println(plane.getWeapon());
        System.out.println(plane.getMaxSpeed());
        //
        System.out.println("*** 飞机变形 ***");
        //
        plane = new CConveyor();
        System.out.println(plane.model);
        System.out.println(plane.getWeapon());
        System.out.println(plane.getMaxSpeed());
    }

代码中,plane对象定义为CPlaneBase类型,但它不能实例化为CPlaneBase类的实例。首先,将plane实例化为CFighter类的对象,显示信息后,又将plane对象实例化为CConveyor类的对象并显示信息。代码执行结果如图3-17所示。

实际上,对于标准的制定者,接口(interface)会更加纯粹,第4章将讨论相关内容。

图3-17 抽象类的综合测试