# 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`,并将所有方法都设为`public`(`protected`成员也允许派生出来的类访问它;以后还会深入探讨这一问题)。当然,在一些特殊的场合,我们仍然必须作出一些调整,但这并不是一个好的做法。
注意`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章会看到的那样,这同时也会防止派生类构造器捕获来自一个基类的任何异常事件。显然,这有时会为我们造成不便。
- Java 编程思想
- 写在前面的话
- 引言
- 第1章 对象入门
- 1.1 抽象的进步
- 1.2 对象的接口
- 1.3 实现方案的隐藏
- 1.4 方案的重复使用
- 1.5 继承:重新使用接口
- 1.6 多态对象的互换使用
- 1.7 对象的创建和存在时间
- 1.8 异常控制:解决错误
- 1.9 多线程
- 1.10 永久性
- 1.11 Java和因特网
- 1.12 分析和设计
- 1.13 Java还是C++
- 第2章 一切都是对象
- 2.1 用引用操纵对象
- 2.2 所有对象都必须创建
- 2.3 绝对不要清除对象
- 2.4 新建数据类型:类
- 2.5 方法、参数和返回值
- 2.6 构建Java程序
- 2.7 我们的第一个Java程序
- 2.8 注释和嵌入文档
- 2.9 编码样式
- 2.10 总结
- 2.11 练习
- 第3章 控制程序流程
- 3.1 使用Java运算符
- 3.2 执行控制
- 3.3 总结
- 3.4 练习
- 第4章 初始化和清除
- 4.1 用构造器自动初始化
- 4.2 方法重载
- 4.3 清除:收尾和垃圾收集
- 4.4 成员初始化
- 4.5 数组初始化
- 4.6 总结
- 4.7 练习
- 第5章 隐藏实现过程
- 5.1 包:库单元
- 5.2 Java访问指示符
- 5.3 接口与实现
- 5.4 类访问
- 5.5 总结
- 5.6 练习
- 第6章 类复用
- 6.1 组合的语法
- 6.2 继承的语法
- 6.3 组合与继承的结合
- 6.4 到底选择组合还是继承
- 6.5 protected
- 6.6 累积开发
- 6.7 向上转换
- 6.8 final关键字
- 6.9 初始化和类装载
- 6.10 总结
- 6.11 练习
- 第7章 多态性
- 7.1 向上转换
- 7.2 深入理解
- 7.3 覆盖与重载
- 7.4 抽象类和方法
- 7.5 接口
- 7.6 内部类
- 7.7 构造器和多态性
- 7.8 通过继承进行设计
- 7.9 总结
- 7.10 练习
- 第8章 对象的容纳
- 8.1 数组
- 8.2 集合
- 8.3 枚举器(迭代器)
- 8.4 集合的类型
- 8.5 排序
- 8.6 通用集合库
- 8.7 新集合
- 8.8 总结
- 8.9 练习
- 第9章 异常差错控制
- 9.1 基本异常
- 9.2 异常的捕获
- 9.3 标准Java异常
- 9.4 创建自己的异常
- 9.5 异常的限制
- 9.6 用finally清除
- 9.7 构造器
- 9.8 异常匹配
- 9.9 总结
- 9.10 练习
- 第10章 Java IO系统
- 10.1 输入和输出
- 10.2 增添属性和有用的接口
- 10.3 本身的缺陷:RandomAccessFile
- 10.4 File类
- 10.5 IO流的典型应用
- 10.6 StreamTokenizer
- 10.7 Java 1.1的IO流
- 10.8 压缩
- 10.9 对象序列化
- 10.10 总结
- 10.11 练习
- 第11章 运行期类型识别
- 11.1 对RTTI的需要
- 11.2 RTTI语法
- 11.3 反射:运行期类信息
- 11.4 总结
- 11.5 练习
- 第12章 传递和返回对象
- 12.1 传递引用
- 12.2 制作本地副本
- 12.3 克隆的控制
- 12.4 只读类
- 12.5 总结
- 12.6 练习
- 第13章 创建窗口和程序片
- 13.1 为何要用AWT?
- 13.2 基本程序片
- 13.3 制作按钮
- 13.4 捕获事件
- 13.5 文本字段
- 13.6 文本区域
- 13.7 标签
- 13.8 复选框
- 13.9 单选钮
- 13.10 下拉列表
- 13.11 列表框
- 13.12 布局的控制
- 13.13 action的替代品
- 13.14 程序片的局限
- 13.15 视窗化应用
- 13.16 新型AWT
- 13.17 Java 1.1用户接口API
- 13.18 可视编程和Beans
- 13.19 Swing入门
- 13.20 总结
- 13.21 练习
- 第14章 多线程
- 14.1 反应灵敏的用户界面
- 14.2 共享有限的资源
- 14.3 堵塞
- 14.4 优先级
- 14.5 回顾runnable
- 14.6 总结
- 14.7 练习
- 第15章 网络编程
- 15.1 机器的标识
- 15.2 套接字
- 15.3 服务多个客户
- 15.4 数据报
- 15.5 一个Web应用
- 15.6 Java与CGI的沟通
- 15.7 用JDBC连接数据库
- 15.8 远程方法
- 15.9 总结
- 15.10 练习
- 第16章 设计模式
- 16.1 模式的概念
- 16.2 观察器模式
- 16.3 模拟垃圾回收站
- 16.4 改进设计
- 16.5 抽象的应用
- 16.6 多重分发
- 16.7 访问器模式
- 16.8 RTTI真的有害吗
- 16.9 总结
- 16.10 练习
- 第17章 项目
- 17.1 文字处理
- 17.2 方法查找工具
- 17.3 复杂性理论
- 17.4 总结
- 17.5 练习
- 附录A 使用非JAVA代码
- 附录B 对比C++和Java
- 附录C Java编程规则
- 附录D 性能
- 附录E 关于垃圾收集的一些话
- 附录F 推荐读物