第七章 继承

165 阅读31分钟

继承是java面向对象编程技术的一块基石,因为它允许创建分等级层次的类。

继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。习惯地称子类与父类是"is-a”关系。父类更通用,子类更具体。

我们来举个例子:动物有很多种,是一个比较大的概念。在动物的种类中,我们熟悉的有猫(Cat)、狗(Dog)等动物,它们都有动物的一般特征(比如能够吃东西,能够发出声音),不过又在细节上有区别(不同动物的吃的不同,叫声不一样)。

在 Java 语言中实现 Cat 和 Dog 等类的时候,就需要继承 Animal 这个类。继承之后 Cat、Dog 等具体动物类就是子类,Animal 类就是父类。

extends-bigsai-bf43b473-4a05-4727-a543-c4edd44e5437.png

7.1 继承基础

要继承一个类,您只需使用关键字extends将一个类的定义合并到另一个类中。一般形式如下:

class 父类 {
}
 
class 子类 extends 父类 {
}

以下程序创建了一个名为A的父类和一个名为B的子类。请注意,关键字extends被用来创建A的子类。

package com.mycompany.simpleinheritance;

//一个简单的继承的示例程序.
class A{
    int i,j;
    
    void showij(){
        System.out.println("i和j为:"+i+" "+j);
    }
}

//创建子类
class B extends A{
    int k;
    
    void showk(){
        System.out.println("k:"+k);
    }
    
    void sum(){
        System.out.println("i+j+k:"+(i+j+k));
    }
}
public class SimpleInheritance {

    public static void main(String[] args) {
        A superOb=new A();
        B subOb=new B();
        
        //父类
        superOb.i=10;
        superOb.j=20;
        System.out.println("父类的内容为:");
        superOb.showij();
        System.out.println();
        
        /*
        子类可以访问所有的父类public成员
        */
        subOb.i=7;
        subOb.j=8;
        subOb.k=9;
        System.out.println("子类的内容为");
        subOb.showij();
        subOb.showk();
        System.out.println();
        
        System.out.println("i,j,k的值和为");
        subOb.sum();
    }
}

它的输出结果为:

父类的和为:
ij为:10 20

子类的内容为
ij为:7 8
k:9

i,j,k的值分别为
i+j+k:24

子类B包含了父类A的所有成员,因此子类B可以访问i和j并调用showij()。在sum()函数中,i和j可以直接引用,就好像它们是B的一部分一样。即使A是B的父类,它也是一个完全独立的、独立存在的类。父类不仅仅是子类的子集,还可以单独使用。此外,子类也可以成为另一个子类的父类。

在Java中,你只能为任何子类指定一个父类。Java不支持将多个父类继承到一个子类中。当然你可以创建一个继承层次结构,在这个结构中,一个子类成为另一个子类的父类。然而,没有任何类可以是其自身的父类。

注意,尽管子类包含其父类的所有成员,但它无法访问那些被声明为私有(private)的父类成员

让我们来看一个更实际的例子。我们接下来会继承在前一章中的Box类的最终版本,以包括一个名为weight的第四个组件。因此,这个新类将包含一个盒子的宽度、高度、深度和重量。

package com.mycompany.demoboxweight;

class Box{
    double width;
    double height;
    double depth;
    
    //这个构造函数用一个对象作为参数
    Box(Box ob){ 
        width=ob.width;
        height=ob.height;
        depth=ob.depth;
    }
    
    //当所有的尺寸都给出时的构造函数
    Box(double w,double h,double d){
        width=w;
        height=h;
        depth=d;
    }
    
    //没有参数的构造函数
    Box(){
        width=-1;
        height=-1;
        depth=-1;
    }
    
    //只有一个参数的时候,我们默认创建了立方体
    Box(double len){
        width=height=depth=len;
    }
    
    //计算体积
    double volume(){
        return width*height*depth;
    }
}

//BoxWeight继承Box类
class BoxWeight extends Box{
    double weight;//盒子的重量
    
    //BoxWeight的构造函数
    BoxWeight(double w, double h,double d, double m){
        width=w;
        height=h;
        depth=d;
        weight=m;
    }
}


public class DemoBoxWeight {

    public static void main(String[] args) {
        BoxWeight mybox1=new BoxWeight(10,20,15,34.3);
        BoxWeight mybox2=new BoxWeight(2,3,4,0.076);
        double vol;
        
        vol=mybox1.volume();
        System.out.println("第一个盒子的体积为:"+vol);
        System.out.println("第一个盒子的重量为:"+mybox1.weight);
        System.out.println();
        
        vol=mybox2.volume();
        System.out.println("第二个盒子的体积为:"+vol);
        System.out.println("第二个盒子的重量为:"+mybox2.weight);
        
        
    }
}

它的输出结果为:

第一个盒子的体积为:3000.0
第一个盒子的重量为:34.3

第二个盒子的体积为:24.0
第二个盒子的重量为:0.076

BoxWeight继承了Box的所有特征,并在其基础上增加了weight成员变量。BoxWeight不需要重新创建Box中的所有功能。它可以简单地扩展Box以满足自己的目的。

继承的一个主要优势是,一旦创建了一个定义了一组对象共有属性的父类,它可以用来创建任意数量的更具体的子类。每个子类都可以精确地调整自己的分类。例如,下面的类继承了Box并添加了一个颜色属性:

//这里Box类
class ColorBox extends Box{
    int colour;//盒子的颜色
    
    ColorBox(double w,double h,double d,int c){
        width=w;
        height=h;
        depth=d;
        color=c;
    }    
}

请记住,一旦创建了定义对象一般特征的父类,该父类可以被继承而形成专门的子类。每个子类只需添加自己独特的属性。这就是继承的本质。

父类的实例可以被赋予指向任何从该父类继承的子类的实例,以下是一个例子帮助理解:

package com.mycompany.refdemo;
//这两个类的定义与继承与上一个程序中的一致
class Box{
    double width;
    double height;
    double depth;
    
    //这个构造函数用一个对象作为参数
    Box(Box ob){ 
        width=ob.width;
        height=ob.height;
        depth=ob.depth;
    }
    
    //当所有的尺寸都给出时的构造函数
    Box(double w,double h,double d){
        width=w;
        height=h;
        depth=d;
    }
    
    //没有参数的构造函数
    Box(){
        width=-1;
        height=-1;
        depth=-1;
    }
    
    //只有一个参数的时候,我们默认创建了立方体
    Box(double len){
        width=height=depth=len;
    }
    
    //计算体积
    double volume(){
        return width*height*depth;
    }
}

//BoxWeight继承Box类
class BoxWeight extends Box{
    double weight;//盒子的重量
    
    //BoxWeight的构造函数
    BoxWeight(double w, double h,double d, double m){
        width=w;
        height=h;
        depth=d;
        weight=m;
    }
}
public class RefDemo {

    public static void main(String[] args) {
        BoxWeight weightbox=new BoxWeight(3,5,7,8.37);
        Box plainbox=new Box();
        double vol;
        
        vol=weightbox.volume();
        System.out.println("weightbox的体积为:"+vol);
        System.out.println("weightbox的重量为:"+weightbox.weight);
        System.out.println();
        
        //直接把子类BoxWeight的实例weightbox赋值给父类Box的实例plainbox
        plainbox=weightbox;
        
        vol=plainbox.volume();
        System.out.println("plainbox的体积为:"+vol);
        
        /*
        注意以下的代码是不可行的因为父类的实例变量并没有成员weight
        System.out.println(plainbox.weight);
        */
        
    }
}

在这里,weightbox是指向BoxWeight类的引用,而plainbox是指向Box类的引用。由于BoxWeight是Box的子类,因此可以将plainbox分配一个指向weightbox对象的引用。**当将子类对象的引用分配给父类引用变量时,父类引用变量只能访问父类定义的对象的部分。**这就是为什么plainbox即使引用了BoxWeight对象也不能访问weight的原因。如果您仔细思考一下,这是有道理的,因为父类不知道子类添加了什么。这就是为什么上面片段中的最后一行代码被注释掉的原因。Box引用无法访问weight字段,因为Box没有定义它。 尽管上述内容可能看起来有些深奥,但它具有一些重要的实际应用,其中两个在本章后面讨论。

7.2 super关键字

BoxWeight子类的构造函数明依然需要初始化Box的宽度、高度和深度字段。这不仅重复了在其父类中的代码,效率低下,而且意味着子类必须被授予访问这些成员的权限。如果我们希望父类Box的宽度、高度和深度字段都是private私有成员,那么这时候我们将要用到关键字super。这样做也是为了实现封装的特征。super关键字是一个指向当前对象的父类特征的引用,它可以用来访问父类的构造方法、属性和方法

7.2.1 使用super调用父类构造函数

一个子类可以使用以下形式的super来调用它的超类定义的构造函数:

super(arg-list);

这里,arg-list指定了父类构造函数所需要的任何参数。super()必须是子类构造函数中执行的第一条语句。

下面我们用改进版的BoxWeight类来说明super的实际用法:

// 父类
class Box {
  double width;
  double height;
  double depth;

  // 构造函数
  Box(double w, double h, double d) {
    width = w;
    height = h;
    depth = d;
  }

  // 计算并返回体积
  double volume() {
    return width * height * depth;
  }
}

// 子类
class BoxWeight extends Box {
  double weight; // 箱子的重量

  // 构造函数
  BoxWeight(double w, double h, double d, double m) {
    super(w, h, d); // 调用超类的构造函数
    weight = m; // 初始化重量
  }
}

在这里,BoxWeight()使用参数w、h和d并调用super()super()将调用Box构造函数,Box构造函数初始化width、height和depth。子类BoxWeight不再自己初始化这些值。它只需要初始化自己特有的值:weight。这使得Box可以自由地将其他的值设为私有(如果需要的话)。

在前面的示例中,super()使用三个参数进行了调用。由于构造函数可以重载,super()可以使用超类定义的任何形式进行调用。执行的构造函数将是与参数匹配的构造函数。例如,以下是一个完整的BoxWeight实现,为构造不同类型的盒子提供构造函数。在每种情况下,使用适当的参数调用super()。请注意,我们在Box内部将width、height和depth设为私有。

package com.mycompany.demosuper;
 // A complete implementation of BoxWeight.
class Box {
  private double width;
  private double height;
  private double depth;

  // construct clone of an object
  Box(Box ob) { // pass object to constructor
    width = ob.width;
    height = ob.height;
    depth = ob.depth;
  }

  // constructor used when all dimensions specified
  Box(double w, double h, double d) {
    width = w;
    height = h;
    depth = d;
  }

  // constructor used when no dimensions specified
  Box() {
    width = -1;  // use -1 to indicate
    height = -1; // an uninitialized
    depth = -1;  // box
  }

  // constructor used when cube is created
  Box(double len) {
    width = height = depth = len;
  }

  // 计算体积
  double volume() {
    return width * height * depth;
  }
}

// BoxWeight类将继承所有的构造函数
class BoxWeight extends Box {
  double weight; // weight of box

  // construct clone of an object
  BoxWeight(BoxWeight ob) { // pass object to constructor
    super(ob);
    weight = ob.weight;
  }

  // constructor when all parameters are specified
  BoxWeight(double w, double h, double d, double m) {
    super(w, h, d); // call superclass constructor
    weight = m;
  }    

  // default constructor
  BoxWeight() {
    super();
    weight = -1;
  }

  // constructor used when cube is created
  BoxWeight(double len, double m) {
    super(len);
    weight = m;
  }
}
  
class DemoSuper {
  public static void main(String args[]) {
    BoxWeight mybox1 = new BoxWeight(10, 20, 15, 34.3);
    BoxWeight mybox2 = new BoxWeight(2, 3, 4, 0.076);
    BoxWeight mybox3 = new BoxWeight(); // default
    BoxWeight mycube = new BoxWeight(3, 2);
    BoxWeight myclone = new BoxWeight(mybox1);
    double vol;

    vol = mybox1.volume();
    System.out.println("Volume of mybox1 is " + vol);
    System.out.println("Weight of mybox1 is " + mybox1.weight);
    System.out.println();

    vol = mybox2.volume();
    System.out.println("Volume of mybox2 is " + vol);
    System.out.println("Weight of mybox2 is " + mybox2.weight);
    System.out.println();

    vol = mybox3.volume();
    System.out.println("Volume of mybox3 is " + vol);
    System.out.println("Weight of mybox3 is " + mybox3.weight);
    System.out.println();
 
    vol = myclone.volume();
    System.out.println("Volume of myclone is " + vol);
    System.out.println("Weight of myclone is " + myclone.weight);
    System.out.println();

    vol = mycube.volume();
    System.out.println("Volume of mycube is " + vol);
    System.out.println("Weight of mycube is " + mycube.weight);
    System.out.println();
  }
}

这个程序的输出为:

Volume of mybox1 is 3000.0
Weight of mybox1 is 34.3

Volume of mybox2 is 24.0
Weight of mybox2 is 0.076

Volume of mybox3 is -1.0
Weight of mybox3 is -1.0

Volume of myclone is 3000.0
Weight of myclone is 34.3

Volume of mycube is 27.0
Weight of mycube is 2.0

首先我们先看到这块代码:

 BoxWeight(BoxWeight ob) { // pass object to constructor
    super(ob);
    weight = ob.weight;

super()接收的是一个子类BoxWeight类型的对象,而不是父类Box类型。super仍然调用了构造函数Box(Box ob)。我们在之前说过父类变量可以用于引用任何从该类派生的子类对象。因此,我们可以将一个BoxWeight对象传递给Box构造函数。当然,Box只知道自己的成员。

当子类调用super()时,它调用的是其直接超类的构造函数。因此,super()始终指的是调用类的上一级父类。即使在多级层次结构中也是如此。此外,super()必须始终是子类构造函数内执行的第一条语句

7.2.2 super的第二种用法

super的第二种形式有点像this,但它总是指向它所在的子类的父类。这种用法的一般形式如下:

super.member

在这里,member可以是一个方法或一个实例变量。 这种形式最适用于子类中的成员名称与父类中相同名称的情况,它可以起到隐藏父类成员的作用。下面我们用一个例子详细说明:

package com.mycompany.usesuper;
//这个程序用super解决了命名冲突
class A{
    int i;
}

//子类继承A
class B extends A{
    int i;//这个i与父类A中的i发生了命名冲突
    B(int a,int b){
        super.i=a;//A中的i
        i=b;//b中的i
    }
    
    void show(){
        System.out.println("在父类中的i:"+super.i);
        System.out.println("在子类中的i:"+i);
    }
}

public class UseSuper {

    public static void main(String[] args) {
        B subOb=new B(1,2);
        subOb.show();
                
    }
}

它的输出为:

在父类中的i1
在子类中的i2

尽管类B中的实例变量i与A类的i命名一致,但是使用super关键字可以访问在父类中定义的i。super关键字可以用来区分子类与父类名字相同的方法和变量。

7.3 创建多层级继承结构

到目前为止,我们使用的是只包含一个父类和一个子类的简单类继承结构。然而,您可以构建包含任意多层继承的层级结构。正如前面提到的,可以完全将一个子类用作另一个子类的父类。例如,假设有三个类称为A、B和C,C可以是B的子类,而B是A的子类。在这种情况下,每个子类都继承了其所有父类中的特性。C继承了B和A的所有方面。 为了了解多层级继承结构的有用性,我们将给出另一个示例程序。在这个程序中,子类BoxWeight被用作创建名为Shipment的子类的父类。Shipment继承了BoxWeight和Box的所有特性,并添加了一个称为cost的字段,用于保存运输box的费用。

package com.mycompany.demoshipment;

// Extend BoxWeight to include shipping costs.

// Start with Box.
class Box {
  private double width;
  private double height;
  private double depth;

  // construct clone of an object
  Box(Box ob) { // pass object to constructor
    width = ob.width;
    height = ob.height;
    depth = ob.depth;
  }

  // constructor used when all dimensions specified
  Box(double w, double h, double d) {
    width = w;
    height = h;
    depth = d;
  }

  // constructor used when no dimensions specified
  Box() {
    width = -1;  // use -1 to indicate
    height = -1; // an uninitialized
    depth = -1;  // box
  }

  // constructor used when cube is created
  Box(double len) {
    width = height = depth = len;
  }

  // compute and return volume
  double volume() {
    return width * height * depth;
  }
}

// Add weight.
class BoxWeight extends Box {
  double weight; // weight of box

  // construct clone of an object
  BoxWeight(BoxWeight ob) { // pass object to constructor
    super(ob);
    weight = ob.weight;
  }

  // constructor when all parameters are specified
  BoxWeight(double w, double h, double d, double m) {
    super(w, h, d); // call superclass constructor
    weight = m;
  }    

  // default constructor
  BoxWeight() {
    super();
    weight = -1;
  }

  // constructor used when cube is created
  BoxWeight(double len, double m) {
    super(len);
    weight = m;
  }
}

// Add shipping costs
class Shipment extends BoxWeight {
  double cost;

  // construct clone of an object
  Shipment(Shipment ob) { // pass object to constructor
    super(ob);
    cost = ob.cost;
  }

  // constructor when all parameters are specified
  Shipment(double w, double h, double d,
            double m, double c) {
    super(w, h, d, m); // call superclass constructor
    cost = c;
  }    

  // default constructor
  Shipment() {
    super();
    cost = -1;
  }

  // constructor used when cube is created
  Shipment(double len, double m, double c) {
    super(len, m);
    cost = c;
  }
}
  
class DemoShipment {
  public static void main(String args[]) {
    Shipment shipment1 = 
               new Shipment(10, 20, 15, 10, 3.41);
    Shipment shipment2 =
               new Shipment(2, 3, 4, 0.76, 1.28);

    double vol;

    vol = shipment1.volume();
    System.out.println("Volume of shipment1 is " + vol);
    System.out.println("Weight of shipment1 is "
                        + shipment1.weight);
    System.out.println("Shipping cost: $" + shipment1.cost);
    System.out.println();

    vol = shipment2.volume();
    System.out.println("Volume of shipment2 is " + vol);
    System.out.println("Weight of shipment2 is "
                        + shipment2.weight);
    System.out.println("Shipping cost: $" + shipment2.cost);
  }
}

它的输出为:

Volume of shipment1 is 3000.0
Weight of shipment1 is 10.0
Shipping cost: $3.41

Volume of shipment2 is 24.0
Weight of shipment2 is 0.76
Shipping cost: $1.28

通过继承,Shipment 可以利用之前定义的 Box 和 BoxWeight 类,它仅需要添加自己特定应用所需的额外信息。这是继承的价值的一部分;它允许代码的重用。

super() 总是指向最近的父类中的构造函数。Shipment 中的 super() 调用了 BoxWeight 中的构造函数。BoxWeight 中的 super() 调用了 Box 中的构造函数。在类层次结构中,如果父类的构造函数需要参数,则所有子类都必须将这些参数“传递上去”。无论子类是否需要自己的参数,都必须这么做。

在Java中我们也可以把不同的类放到不同的文件中,方便阅读,方便管理。

7.4 构造函数的执行顺序

当创建一个层次结构的类时,构成该层次结构的类的构造函数按照继承顺序执行。例如,给定一个名为B的子类和一个名为A的父类,A的构造函数是在B的构造函数之前执行还是之后执行呢?答案是在类层次结构中,构造函数按照继承顺序完成执行,从父类到子类。从父类到子类的顺序对于是否有super()都是成立的,下面的程序说明了构造函数执行的顺序:

package com.mycompany.callingcons;
//展示构造函数的执行顺序

//创建一个父类
class A{
    A(){
        System.out.println("A的构造函数");
    }
}

//对A进行继承
class B extends A{
    B(){
        System.out.println("B的构造函数");
    }
}

//创建子类的子类
class C extends B{
    C(){
        System.out.println("C的构造函数");
    }
}

public class CallingCons {

    public static void main(String[] args) {
        C c=new C();
    }
}

它的输出为:

A的构造函数
B的构造函数
C的构造函数

构造函数按照父子派生顺序执行。 如果你仔细思考一下,构造函数按照父子派生顺序完成执行是有道理的。因为父类看不到子类拓展的内容,它需要执行的任何初始化操作都是独立于子类的,并且父类的初始化条件很可能是子类初始化的先决条件。因此,它必须先完成执行。

7.5 方法重写

在一个类层次结构中,如果子类中的一个方法与其父类中的一个方法具有相同的名称和类型名,那么这就发生了子类方法对父类方法的重写。当在子类内部调用被重写的方法时,它将始终引用子类定义的那个方法的版本。父类定义的方法版本将被隐藏起来。例如以下的例子:

package com.mycompany.override;
//方法重写的示例
class A{
    int i,j;
    A(int a,int b){
        i=a;
        j=b;
    }
    
    //展示i和j
    void show(){
        System.out.println("i和j为:"+i+" "+j);
    }
}

class B extends A{
    int k;
    B(int a,int b,int c){
        super(a,b);
        k=c;
    }
    
    //下面用方法的重写展示k
    void show(){
        System.out.println("k:"+k);
    }
}

public class Override {

    public static void main(String[] args) {
        B subOb=new B(1,2,3);
        subOb.show();
    }
}

它的输出为:

k:3

当在类型为B的对象上调用show()方法时,将使用在B内部定义的show()方法的版本。也就是说,在B内部定义的show()方法覆盖了A中声明的版本。 如果你希望访问被重写方法的父类版本,可以使用super关键字。例如,在下面版本中,在子类中调用了父类版本的show()方法。这样可以显示所有实例变量。

package com.mycompany.override;
//方法重写的示例
class A{
    int i,j;
    A(int a,int b){
        i=a;
        j=b;
    }
    
    //展示i和j
    void show(){
        System.out.println("i和j为:"+i+" "+j);
    }
}

class B extends A{
    int k;
    B(int a,int b,int c){
        super(a,b);
        k=c;
    }
    
    //下面用方法的重写展示k
    void show(){
        //这里调用了父类A的show()函数
        super.show();
        System.out.println("k:"+k);
    }
}

public class Override {

    public static void main(String[] args) {
        B subOb=new B(1,2,3);
        subOb.show();
    }
}

在这里,super.show() 调用了父类版本的 show() 方法。 只有当子类和父类两个方法的名称,参数和类型名完全相同时,才会发生方法重写。如果只是类型或者传递的参数不相同,那么这两个方法只是重载。例如,下面这个修改过的先前示例版本:

package com.mycompany.override1;
//当类型名不同,方法名字相同的时候,仅仅是发生了重载
class A{
    int i,j;
    
    A(int a, int b){
        i=a;
        j=b;
    }
    
    //展示i和j
    void show(){
        System.out.println("i和j为:"+i+" "+j);
    }
}

class B extends A{
    int k;
    B(int a,int b,int c){
        super(a,b);
        k=c;
    }
    
    //重载
    void show(String msg){
        
        System.out.println(msg+k);
    }
}

public class Override1 {

    public static void main(String[] args) {
        B subOb=new B(1,2,3);
        
        subOb.show("k的值为:");
        subOb.show();//调用父类的show()方法
    }
}

它的输出为:

k的值为:3
i和j为:1 2

B 中的 show() 方法接受一个字符串参数。这使得它的c参数名与 A 中的 show() 方法的参数名不同,后者不接受任何参数。因此,不会发生方法重写(或名称隐藏)。相反,B 中的 show() 方法只是对 A 中的 show() 方法进行了重载。

重写(Override)和重载(Overload)是Java多态性的不同表现。重写是子类对父类的允许访问的方法的实现过程进行重新编写,返回值和形参都不能改变。即外壳不变,核心重写。

重载(overloading)是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。最常用的地方就是构造器的重载。

7.6 动态方法调度

尽管前面的示例展示了方法重写的机制,但并未展示其威力。实际上,如果方法重写仅仅是一种命名空间约定,那么它最多只是一个有趣的奇点,而并无实际价值。然而,事实并非如此。方法重写是Java中最强大概念之一的基础:动态方法调度。动态方法调度是指在运行时而非编译时解析对重写方法的调用的机制。动态方法调度之所以重要,是因为这是Java实现运行时多态性的方式。

让我们把这些词拆开,把调度视为决定调用哪个函数(方法)。动态一词表明它是在运行时确定的。用最简单的话来说,我们可以说应该执行哪个函数/方法是在运行时决定的。当我们使用引用在子类中调用被覆盖的方法时,Java 会根据它所引用的对象的类型来决定执行哪个方法。下面我们用一个例子说明动态方法调度:

package com.mycompany.dispatch;

//动态调度的示例
class A{
    void callme(){
        System.out.println("在类A中的callme()方法");
    }
}

class B extends A{
    //重写callme
    void callme(){
        System.out.println("在类B中的callme()方法");
    }
}

class C extends A{
    //继续重写callme
    void callme(){
        System.out.println("在类C中的callme()方法");
    }
}
public class Dispatch {

    public static void main(String[] args) {
        A a = new A(); // object of type A

        B b = new B(); // object of type B

        C c = new C(); // object of type C

        A r; // obtain a reference of type A

        r = a; // r refers to an A object

        r.callme(); // calls A's version of callme

        r = b; // r refers to a B object

        r.callme(); // calls B's version of callme

        r = c; // r refers to a C object

        r.callme(); // calls C's version of callme

    }
}

它的输出为:

在类A中的callme()方法
在类B中的callme()方法
在类C中的callme()方法

这个程序创建了一个名为A的父类和两个它的子类B和C。子类B和C重写了在A中声明的callme()方法。在main()方法中,我们声明了A、B和C类型的对象。此外,声明了一个A类型的引用变量r。然后程序依次将每种类型的对象的引用分配给r,并使用该引用调用callme()方法。根据输出显示,执行的callme()方法的版本是根据调用时所引用的对象的类型来确定的。如果是根据引用变量r的类型来确定,你将会看到三次调用A的callme()方法。

重写方法允许Java支持运行时多态。多态对面向对象编程至关重要,原因是它允许一个通用类(父类)指定通用的方法,同时允许子类定义这些方法的更具体实现。重写方法是Java实现多态中“一个接口,多种方法”方面的另一种方式。 成功应用多态的部分关键在于理解父类和子类形成了一个从较小到较大的层次结构。

父类提供了子类可以直接使用的所有元素。它还定义了派生类必须自己实现的那些方法。这样,子类就有了定义自己方法的灵活性,同时还保持了一致的接口。因此,通过结合继承和重写方法,父类可以定义将被其所有子类使用的方法的一般形式。 动态的、运行时的多态是面向对象设计对代码重用和健壮性带来的最强大的机制之一。现有代码库能够在不重新编译的情况下调用新类实例上的方法,同时保持清晰的抽象接口,这是一种非常强大的工具。

让我们看一个使用方法重写更实际的例子。下面的程序创建了一个名为Figure的父类,它存储二维尺寸宽和高。它还定义了一个名为area()的方法,用于计算对象的面积。该程序从Figure派生出两个子类。第一个是Rectangle,第二个是Triangle。每个子类都重写了area()方法,以分别返回矩形和三角形的面积。

package com.mycompany.findareas;
//运用运行时多态
class Figure{
    double dim1;
    double dim2;
    
    Figure(double a,double b){
        dim1=a;
        dim2=b;
    }
    
    double area(){
        System.out.println("Figure的面积没有定义");
        return 0;
    }
}

class Rectangle extends Figure{
    Rectangle(double a,double b){
        super(a,b);
    }
    
    //对area方法的重写
    double area(){
        System.out.println("长方形的面积");
        return dim1*dim2;
    }
}

class Triangle extends Figure{
    Triangle(double a,double b){
        super(a,b);
    }
    
    //重写三角形的面积
    double area(){
        System.out.println("三角形的面积");
        return dim1*dim2/2;
    }
}
    
    
public class FindAreas {

    public static void main(String[] args) {
        Figure f=new Figure(10,10);
        Rectangle r=new Rectangle(9,5);
        Triangle t=new Triangle(10,8);
        Figure figref;
        
        figref=r;
        System.out.println("面积为"+figref.area());
        
        figref=t;
        System.out.println("面积为"+figref.area());
        
        figref=f;
        System.out.println("面积为"+figref.area());
    }
}

它的输出为:

长方形的面积
面积为45.0
三角形的面积
面积为40.0
Figure的面积没有定义
面积为0.0

通过继承和运行时多态性这两种机制,可以定义一个一致的接口,用于多个不同但相关的对象类型。在这种情况下,如果一个对象是从Figure派生而来的,那么可以通过调用area()方法来获取它的面积。无论使用的是什么类型的图形,该操作的接口都是相同的。

7.7 抽象类

在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。抽象类除了不能实例化对象之外,类的其它功能依然存在,成员变量、成员方法和构造方法的访问方式和普通类一样。

由于抽象类不能实例化对象,所以抽象类必须被继承,才能被使用。它仅定义了一种通用形式,该形式将由其所有子类共享,而每个子类将填充具体细节。在前面的示例中使用的Figure类就是这种情况。area()方法的定义只是一个占位符。它不会计算和显示任何类型对象的面积。

您可以通过添加抽象类型修饰符来要求子类覆盖某些方法。这些方法有时被称为子类的责任,因为它们在父类中没有具体实现。因此,子类必须覆盖重写它们,而不能简单地使用父类中定义的版本。要声明一个抽象方法,我们必须使用以下通用形式:

abstract type name(parameter-list);

抽象方法是没有方法体的,任何包含一个或多个抽象方法的类也必须被声明为抽象类。要声明一个抽象类,只需在类声明的开头使用abstract关键字来修饰class关键字。抽象类不能直接实例化,因此不能使用new运算符直接创建抽象类的对象。这样的对象是无用的,因为抽象类没有完全定义。

以下是一个具有抽象方法的简单示例,和一个实现该方法的类:

package com.mycompany.abstractdemo;

//一个关于抽象类的简单示例
abstract class A{
    abstract void callme();
    
    //有具体实现的方法依旧可以作为抽象类的成员
    void callmetoo(){
        System.out.println("这是一个具体的方法");
    }
}

class B extends A{
    void callme(){
        System.out.println("B的callme函数的具体实现");
    }
}
public class AbstractDemo {

    public static void main(String[] args) {
        B b=new B();
        
        b.callme();
        b.callmetoo();
    }
}

请注意,该程序中没有声明类A的任何对象。正如前面提到的,Java无法实例化抽象类。还有一点要注意:类A实现了一个具体的方法callmetoo()。这是完全可以接受的。抽象类可以包含任意数量的具体实现的方法。

尽管无法使用抽象类来实例化对象,但可以使用它们来创建对象引用,因为Java的运行时多态性是通过使用父类引用来实现的。我们创建对抽象类的引用,使得它可以指向子类对象。您将在下一个示例中看到这个特性的运用。

使用抽象类,您可以改进之前显示的Figure类。由于未定义的二维图形没有有意义的面积概念,因此程序的以下版本在Figure内部将area()声明为抽象方法。当然,这意味着所有派生自Figure的类都必须重写area()方法。

package com.mycompany.abstractareas;

// 抽象方法和抽象类的运用
abstract class Figure {
  double dim1;
  double dim2; 

  Figure(double a, double b) {
    dim1 = a;
    dim2 = b;
  }

  // area 现在是一个抽象的方法
  abstract double area();
}

class Rectangle extends Figure {
  Rectangle(double a, double b) {
    super(a, b);
  }

  // 重写area()方法
  double area() {
    System.out.println("在Rectangle的area()函数里");
    return dim1 * dim2;
  }
}

class Triangle extends Figure {
  Triangle(double a, double b) {
    super(a, b);
  }

  // 重写area()方法
  double area() {
    System.out.println("在Triangle的area()函数里");
    return dim1 * dim2 / 2;
  }
}

class AbstractAreas {
  public static void main(String args[]) {
  // Figure f = new Figure(10, 10); // 现在是非法的
    Rectangle r = new Rectangle(9, 5);
    Triangle t = new Triangle(10, 8);
    
    Figure figref; // 这是合法的,我们没有用new创建一个新的对象

    figref = r;
    System.out.println("面积为: " + figref.area());
    
    figref = t;
    System.out.println("面积为: " + figref.area());
  }
}

正如main()方法中的注释所指示的那样,不再可以声明Figure类型的对象,因为它现在是抽象的。而且,Figure的所有子类都必须重写area()方法。为了验证这一点,您可以尝试创建一个没有重写area()方法的子类。您将收到一个编译时错误。

虽然不可能创建Figure类型的对象,但可以创建Figure类型的引用变量。变量figref声明为对Figure的引用,这意味着它可以用于引用任何派生自Figure的类的对象。正如解释的那样,通过超类引用变量,在运行时解析覆盖的方法。

7.8 final关键字

关键字final有三种用法。首先,它可以用于创建等同于常量的内容。这种用法在前一章节中已经介绍过了。final的另外两种用法与继承有关,这是接下来我们讨论的重点。

7.8.1 防止方法重写

虽然方法重写是Java最强大的特性之一,但有时您希望防止方法额冲洗。为了禁止某个方法被重写,可以在其声明开始处指定final作为修饰符。声明为final的方法不能被重写。下面的片段演示了final的用法:

class A{
    final void meth() {
        System.out.println("这是一个由final修饰的方法");
    }
}

class B extends class A{
    void meth(){
        //错误,这个方法不能被重写!
    }
}

由于meth()在父类A中被声明为final,在子类B中它无法被重写。如果你尝试这样做,会导致编译时错误。

声明为final的方法有时可以提供性能优化:编译器可以自由地内联调用它们,因为它“知道”它们不会被子类重写。当调用一个小的final方法时,通常Java编译器可以将子例程的字节码直接内联到调用方法的编译代码中,从而消除了与方法调用相关的昂贵开销。内联只适用于final方法。通常,Java在运行时动态解析方法调用,这被称为晚期绑定。然而,由于final方法无法被重写,对它的调用可以在编译时解析,这被称为早期绑定。

7.8.2 防止类的继承

有时您希望防止某个类被继承。为了实现这一点,可以在类声明之前加上final关键字。将一个类声明为final会隐式地将其所有方法也声明为final。将一个类同时声明为抽象abstract和final是不合法的,因为抽象类本身是不完整的,它依赖于其子类提供完整的实现。以下是一个final类的示例:

final class FinalClass {
    // 类的内容
}

// 编译错误,无法继承最终类
class SubClass extends FinalClass {
    // 类的内容
}

正如注释所写,Java不允许继承一个由final修饰的最终类。

7.9 局部变量类型推断和继承

正如在第三章中所介绍,JDK 10向Java语言添加了局部变量类型推断的功能,我们可以使用保留类型名var来使用这个功能。在继承层次结构中,清楚地理解类型推断的工作原理非常重要。父类引用可以引用派生的子类对象,这是Java对多态性的支持的一部分。

然而在使用局部变量类型推断时,变量的推断类型是基于其初始化器的声明类型的。因此,如果初始化器的类型是父类类型,那么变量的推断类型也将是父类类型。无论初始化器引用的实际对象是否是派生子类的实例,这都不重要。

假设我们有一个基类Animal(动物)和一个派生类Cat(猫),它继承自Animal类。我们可以使用局部变量类型推断来演示这一点。请看下面的代码示例:

class Animal {
    public void eat() {
        System.out.println("Animal is eating");
    }
}

class Cat extends Animal {
    @Override
    public void eat() {
        System.out.println("Cat is eating");
    }
}

public class Main {
    public static void main(String[] args) {
        var animal = new Cat();
        animal.eat();
    }
}

在这个例子中,我们创建了一个Animal类和一个Cat类,Cat类是Animal类的子类。在main方法中,我们使用局部变量类型推断来声明一个变量animal,并将其初始化为一个Cat对象。

由于Cat是Animal的子类,Cat对象可以赋值给Animal类型的变量。尽管我们使用了var关键字进行类型推断,但变量animal的推断类型仍然是Animal。new Cat() 初始化器创建的是一个 Cat 类型的对象,但是由于变量的声明类型没有指定,编译器会采用默认的规则,将推断类型设置为初始化器对象的最接近的父类类型。

当我们调用animal.eat()时,尽管实际上是调用了Cat类的eat()方法,但由于变量animal的推断类型是Animal,所以会输出"Animal is eating"。

这个例子展示了即使使用了局部变量类型推断,推断类型仍然是基于变量的声明类型而不是实际对象的类型。这是因为编译器在类型推断时只关注变量的声明类型,并不考虑实际赋值的对象类型。

7.10 Object类

Java中有一个特殊的类,Object类。所有其他类都是Object类的子类。换句话说,Object是所有其他类的父类。这意味着类型为Object的引用变量可以引用任何其他类的对象。此外,由于数组被实现为类,类型为Object的变量也可以引用任何数组。

classes-object.gif

Object 类位于 java.lang 包中,编译时会自动导入,我们创建一个类时,如果没有明确继承一个父类,那么它就会自动继承 Object,成为 Object 的子类。

Object 类可以显式继承,也可以隐式继承,以下两种方式是一样的:

显式继承:

public class NewClass extends Object{

}

隐式继承:

public class NewClass {

}

由于 Java 所有的类都是 Object 类的子类,所以任何 Java 对象都可以调用 Object 类的方法。常见的方法如下表所示。

方法说明
Object clone()创建与该对象的类相同的新对象
boolean equals(Object)比较两对象是否相等
void finalize()当垃圾回收器确定不存在对该对象的更多引用时,对象垃圾回收器调用该方法
Class getClass()返回一个对象运行时的实例类
int hashCode()返回该对象的散列码值
void notify()激活等待在该对象的监视器上的一个线程
void notifyAll()激活等待在该对象的监视器上的全部线程
String toString()返回该对象的字符串表示
void wait()在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待

这些方法中,getClass( )notify( )notifyAll( )wait( )被声明为final,不能被重写。其他方法可以被重写。这些方法在本书的其他地方有详细描述。

equals( )方法用于比较两个对象。如果对象相等,它返回true,否则返回false。关于相等的精确定义可以根据所比较的对象类型而有所不同。toString( )方法返回一个包含调用它的对象描述的字符串。当使用println( )输出一个对象时,这个方法会自动被调用。许多类都会重写这个方法,这样可以为它们创建的对象类型定制特定的描述。

最后一个注意点是getClass( )方法的返回类型中的不寻常的语法。这与Java的泛型特性有关,该特性在后续章节中有描述。