复用类就是指在不复制代码的前提下,通过某种手段创建新类来复用代码。作者本章介绍了两种手段:组合和继承,此外,还介绍了一种介于组合和继承之间的方法:代理。

一、组合语法
class WaterSource {
private String s;
WaterSource() {
System.out.println("WaterSource()");
s = "Constructed";
}
}
private class SprinklerSystem {
private String value;
private WaterSource source = new WaterSource();
}
二、继承语法
2.1.语法
class Cleaner {
private String s = "Cleaner";
public void append(String a) { s += a; }
public void scrub() {
append("scrub()");
}
}
public class Detergent extends Cleaner {
public void scrub() {
append("Detergent.scrub()");
super.scrub();
}
}
- 按照惯例,基类中的方法都定义为 private,方法都定义为 public;
- 基类的方法要想被导出类使用,必须定义为 public 的;
- 如果基类和导出类的方法名称重复时,导出类想显式使用基类中的方法,使用 super 关键字;
2.2.初始化基类
继承并不只是复制基类的接口,当创建一个导出类的对象时,该对象包含了一个基类的子对象。这个子对象和你用基类直接创建的对象是一样的,二者的区别就在于直接创建的基类对象来自外部,而基类的子对象被包装在导出类对象的内部。
导出类&基类初始化顺序:
- 导出类内部调用基类构造器,初始化基类子对象;
- 导出类调用自己的构造器,初始化导出类;
如果是默认构造器,编译器会为我们完成第 1、2 步,如果是带参数的构造器,我们必须要用 super 显示调用基类的构造器,否则编译器会报错。
2.3.清理
我们在《Java编程思想笔记二:初始化与清理》 里提过,在 Java 中的对象可以依靠垃圾回收器在必要的时候释放其内存,但我们并不知道垃圾回收器何时工作。因此,当我们想要在某些时候清理一些东西,就必须手动来完成。按照常规,这个方法一般放在 finally 子句中,以预防异常出现。
清理方法中,需要注意基类清理方法和成员对象清理方法的调用顺序:
- 先清理执行类中的所有对象,对象的清理顺序和生成顺序正好相反(后构造的先清理);
- 调用基类的清理方法。
2.4.名称屏蔽
我们在使用继承时,导出类和基类经常会有形同名称、相同入参的方法,这经常会让人疑惑。如果想避免这种情况出现,可以在继承类中定义方法时用 @Override 注解:
class Lisa extends Homer {
@Override
void doh(Milhouse m) {
System.out.println("doh(Milhouse m)");
}
}
如果基类 Homer 中也存在 doh(Milhouse m) 方法,编译时就会报错,这样可以有效避免上述疑惑,这一点我们在项目开发中基本都会用到。
2.5.向上转型
将导出类引用转换为基类引用的动作我们称为向上转型。之所以称为向上转型,是以传统的类继承图的绘制方法为基础的:

由导出类(Wind)转型成基类(Instrument),在继承图中是由下而上移动的,因此称为向上转型。向上转型总是安全的。
到底该用组合还是继承,一个最清晰的判断就是是否需要向上转型,如果需要则要使用继承。
三、代理语法
代理关系是介于继承和组合之间的中庸之道。代理指将一个成员对象置于所要构造的类中(就像组合),但与此同时,在新类中暴露了该成员对象的所有方法(就像继承)。例如,太空船需要一个控制模块:
public class SpaceShipControls {
void up(int velocity) {}
void down(int velocity) {}
}
制造太空船的一种方式是使用继承:
public class SpaceShip extends SpaceShipControls {
private String name;
public SpaceShip (String name) { this.name = name; }
}
但是这其实并不符合我们的预期,其实太空舱和控制模块是包含关系,而不是 is-a (继承)的关系,而且我们在 SpaceShipControls 类中并不希望把 SpaceShip 类中的所有方法都暴露出去。代理可以解决此难题:
public class SpaceShipDelegation {
private String name;
private SpaceShipControls controls = new SpaceShipControls();
public SpaceShipDelegation(String name) {
this.name = name;
}
public void up(int velocity) {
controls.up(velocity);
}
public void down(int velocity) {
controls.down(velocity);
}
}
上面的代理方式实现了和继承一样的效果,SpaceShipDelegation 类拥有了 SpaceShipControls 类的所有方法。但是,代理更加灵活,比如我们不想把 down() 方法暴露出去,代理类就可以不定义 public void down(int velocity),但是继承做不到这一点。
四、final 关键字
final 关键字主要有三种使用情况:数据、方法和类。
4.1.final 数据
final 修饰基本类型和引用的区别
final 修饰某个成员时,这个成员如果是基本类型,可以实现下面两种效果:
- 一个永远不变的编译时常量;
- 一个在运行时被初始化的值,而你并不希望它被改变。
另外,一个既是 static 又是 final 的域,只占据一段不能改变的存储空间。
final 也可以修饰对象引用,final 会使引用恒定不变。一旦引用被初始化指向一个对象,就无法再把它指向另一个对象,但是对象本身的内容是可以被修改的。这一限制同样适用于数组,它也是对象。下面的示例展示了上述三种情况:
class Value { // 包访问权限
int i;
public Value(int i) { this.i = i; }
}
public class FinalData {
private final int valueOne = 1;
private static final int VALUE_TWO = 2;
public static final int VALUE_THREE = 3;
private final Value v4 = new Value(4);
private static final Value VAL_5 = new Value(5);
private final int[] a = { 1, 2, 3 };
public static void main(String[] args) {
FinalData fd1 = new FinalData();
// fd1.valueOne++; -- 错误:编译器常量,不可修改
// fd1.VALUE_TWO++;
// fd1.VALUE_THREE++;
// fd1.v4 = new Value(44); -- 错误:final引用,不可修改引用指向的对象
// fd1.VAL_5 = new Value(55);
// fd1.a = new int[5];
for(int i=0; i<fd1.a.length; i++)
fd1.a[i]++; // -- 正确:final引用,不可修改引用指向的对象,但可以修改内容
}
}
空白 final
Java 允许生成空白 final,即被声明为 final 但又未给定初始值的域。但是必须在使用前进行初始化:
class class BlankFinal {
private final int i = 0;
private final int j; // 空白 final
public BlankFinal() {
j = 1; // 空白 final 在使用前要进行初始化
}
}
所以,无论是否是空白 final,必须在域的定义处或者构造器中要对 final 进行赋值!
final 参数
Java 允许在参数列表中以声明的方式将参数指明为 final,在方法中该参数为只读参数,无法被修改:
public class FinalArguments {
private Integer age;
public Integer add(final Integer year) {
// year += age; -- 错误:year 为只读参数,不可修改
return year + age;
}
}
4.2. final 方法
指明为 final 的方法在继承体系中,不会被导出类覆盖。
类中所有 private 方法都隐式指定为 final 的,由于导出类无法取用 private 方法,所以也就无法覆盖它。
给 private 方法添加 final 修饰词,不会增加任何额外意义。
导出类为什么不能覆盖基类的 private 方法?
class WithFinals {
private final void f() { print("WithFinal.f()"); }
}
class OverridingPrivate extends WithFinals {
private final void f() { print("OverridingPrivate.f()"); }
}
基类的 private 方法 f() 并不是基类接口的一部分,只是隐藏于内部的一块儿代码。导出类的 public/protected 方法 f() 是一个全新的方法,仅仅是和基类的 private 方法同名罢了,没有任何关系,也不会发生覆盖。
4.3. final 类
当类为 final 时,不允许继承该类。
final 类中的所有方法都隐式指定为 final 的,因为无法覆盖它们。在 final 类中可以给方法添加 fianl 修饰词,但不会添加任何意义。
final 类的域可以根据个人意愿设置是否为 final。
五、继承的初始化顺序
- 在其他任何事情发生之前,将分配给对象的存储空间初始化成二进制的零;
- 调用基类构造器;
- 按照声明顺序调用成员的初始化方法;
- 调用导出类构造器主体。
网友评论