02_oo_04_inheritance

第十七章 继承(Inheritance)

1 简介

在实现了一个类之后,我们可以使用继承特性来扩展这个类的功能,并且复用类中的成员变量和成员方法。

例如,当我们已经定义了Student类之后,我们可以再定义一个Freshman类,用于表示大一新生。Freshman类继承自Student类。在声明类Freshman时,我们使用了关键字extends表示Freshman继承自类Student。因此,在此继承关系中,类Student被称为基类(Base Class)或者父类(Parent Class),而类Freshman被称为派生类(Derived Class)或者子类(SubClass)

Java语言规定,一个子类只能继承自一个父类。我们将在下一章讨论Java语言为什么不支持多继承

public class Student {
    private int id;
    
    public Student (int id) {
        this.id = id;
    }

    public int getId() {
        return this.id;
    }

    public void setId(int id) {
        this.id = id;
    }
}

public class Freshman extends Student {
    private int age;
    ...
}

2 子类

我们称子类"继承自"父类的原因是子类继承了父类中的成员变量和成员方法。换句话说,一个Freshman对象包含了两个成员变量id和age,同时也包含了Student类和Freshman类中定义的方法。

Student类中使用的访问控制关键字(public、protected、private)明确指出了哪些成员变量和成员方法可以在子类Freshman中访问。根据Java语言的访问控制规则,子类能够访问父类中声明为public或者protected的成员变量和成员方法。虽然子类不能直接访问父类声明的私有成员(private),但是这并不表示这些成员不存在于子类的对象中。这些私有成员可以通过调用父类提供的public或者protected方法访问。

在子类对象的构造过程中,Java语言会从父类的构造函数开始逐级地向下调用各个层级的构造函数。在子类的构造函数中,可以通过使用super()显式地指明调用哪个父类的构造函数来初始化父类中的成员变量。super()语句必须出现于子类构造函数中的第一行。如果子类没有显式调用父类构造函数的话,Java程序会自动调用父类的无参数构造函数。

public class Freshman extends Student {
    private int age;
    
    public Freshman(int id, int age) {
        super(id);
        this.age = age;
    }
}

我们还可以继续"延展"继承等级。Freshman不仅可以继承Student,它还可以被其他类继承。例如,我们还可以创建一个新类OutstandingFreshman;它继承自Freshman,表示优秀的大一新生。所以,OutstandingFreshman不仅继承了Freshman和Student类声明的成员变量和方法,它还声明了新的成员变量awards,用于表示优秀大一新生所获得的奖项。

public class OutstandingFreshman extends Freshman {
    private Award[] awards = null;
    ...

    OutstandingFreshman(int id, int age) {
        super(id, age);
    }
}

从设计上看,如果我们不想OutstandingFreshman再被其他类继承的话,我们可以声明OutstandingFreshman是final的。因此,其他类就不能继承OutstandingFreshman了。

final public class OutstandingFreshman extends Freshman {
    private Award[] awards = null;
    ...
}

关键字final还可以用于成员方法,表示该成员方法不能被子类的成员方法覆盖(Override)。

public class Student {
    private int id;
    
    public Student (int id) {
        this.id = id;
    }

    final public int getId() {
        return this.id;
    }

    final public void setId(int id) {
        this.id = id;
    }
}

3 类型转换(Type Casting)

在开发过程中,我们常常需要使用类型转换。将子类类型转换为基类类型或者接口类型是非常自然的,不需要开发人员额外的进行任何强制类型转换。例如,在下面的代码示例中,我们可以将Freshman对象赋值给Student引用类型的变量,因为Freshman对象也是一个Student类型的对象。

Student s = new Freshman(1010, 22);

但是,如果将一个基类类型的变量转换成子类类型的变量,开发人员需要使用强制类型转换。在转换过程中,Java虚拟机会检查类型转换是否能成功执行。当类型转换失败时,程序会收到ClassCastException异常。

类型转换有如下两种写法。第一种是直接将类型转换的目标类型放在一对括号中,Java编译器会将其翻译成相应的类型转换逻辑。第二种方法是调用目标类的cast()成员方法,完成类型转换。第一种方法也能用于基本数据类型的强制类型转换

Student s = new OutstandingFreshman(1010, 22);
Freshman f = (Freshman)s;
OutstandingFreshman o = OutstandingFreshman.class.cast(f);

为了避免类型转换失败,收到ClassCastException异常,在运行类型转换之前,我们可以先判断一下对象的类型。这个判断的过程可由instanceof操作符完成,或者使用目标类的isinstance()成员方法完成。这两种方法的实现逻辑十分相似。唯一的区别是instanceof是操作符,由编译器处理。而isinstance()方法则是程序运行时调用的方法。

Student s = new OutstandingFreshman(1010, 22);
if (s instanceof Freshman) {
    Freshman f = (Freshman)s;
    ...
}

if (OutstandingFreshman.class.isinstance(f)) {
    OutstandingFreshman o = OutstandingFreshman.class.cast(f);
    ...
}

4 Object类

Java语言为所有的对象提供了一个"超级"父类java.lang.Object类。Object类是Java语言提供的一个预定义类(Predefined Class)。当开发人员未明确指出继承的父类时,这个类会默认的继承自Object类。所以,在我们的例子中,Student类继承自Object类,而且我们在声明Student类时,不需要明确地使用extends Object来声明这个继承关系。Java编译器会帮我们完成。

Student类是Object类的子类;Freshman类是Object类的"孙子"类,以此类推。因此,在Java程序中,所有的类都直接或者间接继承自Object类。Object类是所有类的"超级"父类。

public class Student {
    private int id;
    
    public Student (int id) {
        this.id = id;
    }

    final public int getId() {
        return this.id;
    }

    final public void setId(int id) {
        this.id = id;
    }
}

Object类定义了一些重要的成员方法。它们很重要是因为所有的类都会继承这些成员方法。其中,equals()方法用于比较两个对象是否相等。clone()方法用于生成一个对象的拷贝。当对象被Java虚拟机的垃圾回收器回收时,会调用finalize(),以进行额外的一些垃圾回收处理。getClass()方法用于在运行时获得对象的信息。wait()和notify()方法可用于多线程的Java程序设计之中。我们会在后续章节中逐一介绍它们。

hashCode()方法用于根据对象的内容生成一个哈希值(Hash Code Value)。这个方法被用于HashMap容器,快速判断对象是否相同。hashCode()方法的特点是给定两个对象,如果它们是同一个对象的话,它们的哈希值一定相同。如果它们是两个不同的对象的话,它们的哈希值可能相同,也可能不同。但是,它们有着不同哈希值的可能性非常大。

另一个重要的方法是toString()。该方法用于根据对象的内容生成一个字符串对象。这个方法常用于日志功能,打印对象的内容。在子类中,我们常常建议覆盖基类的toString()方法,以便于在生成的字符串中包含子类的内容。

5 小结

继承特性帮助开发人员更好的复用代码功能。在继承了一个父类之后,子类也继承了父类中声明的变量和方法。在Java语言中,所有的类都直接或者间接继承自Object类,以便于实现对象的比较、查找、保护和打印等功能。

上一章
下一章

注册用户登陆后可留言

Copyright  2019 Little Waterdrop, LLC. All Rights Reserved.