# 6.2 继承的语法

继承与Java(以及其他OOP语言)非常紧密地结合在一起。我们早在第1章就为大家引入了继承的概念,并在那章之后到本章之前的各章里不时用到,因为一些特殊的场合要求必须使用继承。除此以外,创建一个类时肯定会进行继承,因为若非如此,会从Java的标准根类Object中继承。

用于组合的语法是非常简单且直观的。但为了进行继承,必须采用一种全然不同的形式。需要继承的时候,我们会说:“这个新类和那个旧类差不多。”为了在代码里表面这一观念,需要给出类名。但在类主体的起始花括号之前,需要放置一个关键字extends,在后面跟随“基类”的名字。若采取这种做法,就可自动获得基类的所有数据成员以及方法。下面是一个例子:

//: Detergent.java
// Inheritance syntax & properties

class Cleanser {
  private String s = new String("Cleanser");
  public void append(String a) { s += a; }
  public void dilute() { append(" dilute()"); }
  public void apply() { append(" apply()"); }
  public void scrub() { append(" scrub()"); }
  public void print() { System.out.println(s); }
  public static void main(String[] args) {
    Cleanser x = new Cleanser();
    x.dilute(); x.apply(); x.scrub();
    x.print();
  }
}

public class Detergent extends Cleanser {
  // Change a method:
  public void scrub() {
    append(" Detergent.scrub()");
    super.scrub(); // Call base-class version
  }
  // Add methods to the interface:
  public void foam() { append(" foam()"); }
  // Test the new class:
  public static void main(String[] args) {
    Detergent x = new Detergent();
    x.dilute();
    x.apply();
    x.scrub();
    x.foam();
    x.print();
    System.out.println("Testing base class:");
    Cleanser.main(args);
  }
} ///:~

这个例子向大家展示了大量特性。首先,在Cleanser append()方法里,字符串同一个s连接起来。这是用+=运算符实现的。同+一样,+=被Java用于对字符串进行“重载”处理。

其次,无论Cleanser还是Detergent都包含了一个main()方法。我们可为自己的每个类都创建一个main()。通常建议大家象这样进行编写代码,使自己的测试代码能够封装到类内。即便在程序中含有数量众多的类,但对于在命令行请求的public类,只有main()才会得到调用。所以在这种情况下,当我们使用java Detergent的时候,调用的是Degergent.main()——即使Cleanser并非一个public类。采用这种将main()置入每个类的做法,可方便地为每个类都进行单元测试。而且在完成测试以后,毋需将main()删去;可把它保留下来,用于以后的测试。

在这里,大家可看到Deteregent.main()Cleanser.main()的调用是明确进行的。

需要着重强调的是Cleanser中的所有类都是public属性。请记住,倘若省略所有访问指示符,则成员默认为“友好的”。这样一来,就只允许对包成员进行访问。在这个包内,任何人都可使用那些没有访问指示符的方法。例如,Detergent将不会遇到任何麻烦。然而,假设来自另外某个包的类准备继承Cleanser,它就只能访问那些public成员。所以在计划继承的时候,一个比较好的规则是将所有字段都设为private,并将所有方法都设为publicprotected成员也允许派生出来的类访问它;以后还会深入探讨这一问题)。当然,在一些特殊的场合,我们仍然必须作出一些调整,但这并不是一个好的做法。

注意Cleanser在它的接口中含有一系列方法:append()dilute()apply()scrub()以及print()。由于Detergent是从Cleanser派生出来的(通过extends关键字),所以它会自动获得接口内的所有这些方法——即使我们在Detergent里并未看到对它们的明确定义。这样一来,就可将继承想象成“对接口的重复利用”或者“接口的复用”(以后的实现细节可以自由设置,但那并非我们强调的重点)。

正如在scrub()里看到的那样,可以获得在基类里定义的一个方法,并对其进行修改。在这种情况下,我们通常想在新版本里调用来自基类的方法。但在scrub()里,不可只是简单地发出对scrub()的调用。那样便造成了递归调用,我们不愿看到这一情况。为解决这个问题,Java提供了一个super关键字,它引用当前类已从中继承的一个“超类”(Superclass)。所以表达式super.scrub()调用的是方法scrub()的基类版本。

进行继承时,我们并不限于只能使用基类的方法。亦可在派生出来的类里加入自己的新方法。这时采取的做法与在普通类里添加其他任何方法是完全一样的:只需简单地定义它即可。extends关键字提醒我们准备将新方法加入基类的接口里,对其进行“扩展”。foam()便是这种做法的一个产物。

Detergent.main()里,我们可看到对于Detergent对象,可调用Cleanser以及Detergent内所有可用的方法(如foam())。

# 6.2.1 初始化基类

由于这儿涉及到两个类——基类及派生类,而不再是以前的一个,所以在想象派生类的结果对象时,可能会产生一些迷惑。从外部看,似乎新类拥有与基类相同的接口,而且可包含一些额外的方法和字段。但继承并非仅仅简单地复制基类的接口了事。创建派生类的一个对象时,它在其中包含了基类的一个“子对象”。这个子对象就象我们根据基类本身创建了它的一个对象。从外部看,基类的子对象已封装到派生类的对象里了。

当然,基类子对象应该正确地初始化,而且只有一种方法能保证这一点:在构造器中执行初始化,通过调用基类构造器,后者有足够的能力和权限来执行对基类的初始化。在派生类的构造器中,Java会自动插入对基类构造器的调用。下面这个例子向大家展示了对这种三级继承的应用:

//: Cartoon.java
// Constructor calls during inheritance

class Art {
  Art() {
    System.out.println("Art constructor");
  }
}

class Drawing extends Art {
  Drawing() {
    System.out.println("Drawing constructor");
  }
}

public class Cartoon extends Drawing {
  Cartoon() {
    System.out.println("Cartoon constructor");
  }
  public static void main(String[] args) {
    Cartoon x = new Cartoon();
  }
} ///:~

该程序的输出显示了自动调用:

Art constructor
Drawing constructor
Cartoon constructor

可以看出,构建是在基类的“外部”进行的,所以基类会在派生类访问它之前得到正确的初始化。 即使没有为Cartoon()创建一个构造器,编译器也会为我们自动生成一个默认构造器,并发出对基类构造器的调用。

(1) 含有参数的构造器

上述例子有自己默认的构造器;也就是说,它们不含任何参数。编译器可以很容易地调用它们,因为不存在具体传递什么参数的问题。如果类没有默认的参数,或者想调用含有一个参数的某个基类构造器,必须明确地编写对基类的调用代码。这是用super关键字以及适当的参数列表实现的,如下所示:

//: Chess.java
// Inheritance, constructors and arguments

class Game {
  Game(int i) {
    System.out.println("Game constructor");
  }
}

class BoardGame extends Game {
  BoardGame(int i) {
    super(i);
    System.out.println("BoardGame constructor");
  }
}

public class Chess extends BoardGame {
  Chess() {
    super(11);
    System.out.println("Chess constructor");
  }
  public static void main(String[] args) {
    Chess x = new Chess();
  }
} ///:~

如果不调用BoardGames()内的基类构造器,编译器就会报告自己找不到Games()形式的一个构造器。除此以外,在派生类构造器中,对基类构造器的调用是必须做的第一件事情(如操作失当,编译器会向我们指出)。

(2) 捕获基本构造器的异常

正如刚才指出的那样,编译器会强迫我们在派生类构造器的主体中首先设置对基类构造器的调用。这意味着在它之前不能出现任何东西。正如大家在第9章会看到的那样,这同时也会防止派生类构造器捕获来自一个基类的任何异常事件。显然,这有时会为我们造成不便。