Skip to content

Commit 7489758

Browse files
committed
[feat 08](完结第8章 复用)
1 parent 9cf274c commit 7489758

File tree

2 files changed

+86
-1
lines changed

2 files changed

+86
-1
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
- [x] [第五章 控制流](docs/book/05-Control-Flow.md)
2828
- [x] [第六章 初始化和清理](docs/book/06-Housekeeping.md)
2929
- [x] [第七章 封装](docs/book/07-Implementation-Hiding.md)
30-
- [ ] [第八章 复用](docs/book/08-Reuse.md)
30+
- [x] [第八章 复用](docs/book/08-Reuse.md)
3131
- [ ] [第九章 多态](docs/book/09-Polymorphism.md)
3232
- [ ] [第十章 接口](docs/book/10-Interfaces.md)
3333
- [x] [第十一章 内部类](docs/book/11-Inner-Classes.md)

docs/book/08-Reuse.md

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1165,14 +1165,99 @@ public class Jurassic {
11651165

11661166
### final 忠告
11671167

1168+
在设计类时将一个方法指明为 final 看上去是明智的。你可能会觉得没人会覆写那个方法。有时这是对的。
1169+
1170+
但请留意你的假设。通常来说,预见一个类如何被复用是很困难的,特别是通用类。如果将一个方法指定为 **final**,可能会防止其他程序员的项目中通过继承来复用你的类,而这仅仅是因为你没有想到它被以那种方式使用。
1171+
1172+
Java 标准类库就是一个很好的例子。尤其是 Java 1.0/1.1 的 **Vector** 类被广泛地使用,而且从效率考虑(这近乎是个幻想),如果它的所有方法没有被指定为 **final**,可能会更加有用。很容易想到,你可能会继承并覆写这么一个基础类,但是设计者们认为这么做不合适。有两个讽刺的原因。第一,**Stack** 继承自 **Vector**,就是说 **Stack** 是个 **Vector**,但从逻辑上来说不对。尽管如此,Java 设计者们仍然这么做,在用这种方式创建 **Stack** 时,他们应该意识到了 **final** 方法过于约束。
1173+
1174+
第二,**Vector** 中的很多重要方法,比如 `addElement()``elementAt()` 方法都是同步的。在“并发编程”一章中会看同步会导致很大的执行开销,可能会抹煞 **final** 带来的好处。这加强了程序员永远无法正确猜到优化应该发生在何处的观点。如此笨拙的设计却出现在每个人都要使用的标准库中,太糟糕了。庆幸的是,现代 Java 容器用 **ArrayList** 代替了 **Vector**,它的行为要合理得多。不幸的是,仍然有很多新代码使用旧的集合类库,其中就包括 **Vector**
1175+
1176+
Java 1.0/1.1 标准类库中另一个重要的类是 **Hashtable**(后来被 **HashMap** 取代),它不含任何 **final** 方法。本书中其他地方也提到,很明显不同的类是由不同的人设计的。**Hashtable** 就比 **Vector** 中的方法名简洁得多,这又是一条证据。对于类库的使用者来说,这是一个本不应该如此草率的事情。这种不规则的情况造成用户需要做更多的工作——这是对粗糙的设计和代码的又一讽刺。
1177+
11681178
<!-- Initialization and Class Loading -->
11691179

11701180
## 类初始化和加载
11711181

1182+
在许多传统语言中,程序在启动时一次性全部加载。接着初始化,然后程序开始运行。必须仔细控制这些语言的初始化过程,以确保 **statics** 初始化的顺序不会造成麻烦。在 C++ 中,如果一个 **static** 期望使用另一个 **static**,而另一个 **static** 还没有初始化,就会出现问题。
1183+
1184+
Java 中不存在这样的问题,因为它采用了一种不同的方式加载。因为 Java 中万物皆对象,所以加载活动就容易得多。记住每个类的编译代码都存在于它自己独立的文件中。该文件只有在使用程序代码时才会被加载。一般可以说“类的代码在首次使用时加载“。这通常是指创建类的第一个对象,或者是访问了类的 **static** 属性或方法。构造器也是一个 **static** 方法尽管它的 **static** 关键字是隐式的。因此,准确地说,一个类当它任意一个 **static** 成员被访问时,就会被加载。
1185+
1186+
首次使用时就是 **static** 初始化发生时。所有的 **static** 对象和 **static** 代码块在加载时按照文本的顺序(在类中定义的顺序)依次初始化。**static** 变量只被初始化一次。
1187+
1188+
### 继承和初始化
1189+
1190+
了解包括继承在内的整个初始化过程是有帮助的,这样可以对所发生的一切有全局性的把握。考虑下面的例子:
1191+
1192+
```java
1193+
// reuse/Beetle.java
1194+
// The full process of initialization
1195+
class Insect {
1196+
private int i = 9;
1197+
protected int j;
1198+
1199+
Insect() {
1200+
System.out.println("i = " + i + ", j = " + j);
1201+
j = 39;
1202+
}
1203+
1204+
private static int x1 = printInit("static Insect.x1 initialized");
1205+
1206+
static int printInit(String s) {
1207+
System.out.println(s);
1208+
return 47;
1209+
}
1210+
}
1211+
1212+
public class Beetle extends Insect {
1213+
private int k = printInit("Beetle.k.initialized");
1214+
1215+
public Beetle() {
1216+
System.out.println("k = " + k);
1217+
System.out.println("j = " + j);
1218+
}
1219+
1220+
private static int x2 = printInit("static Beetle.x2 initialized");
1221+
1222+
public static void main(String[] args) {
1223+
System.out.println("Beetle constructor");
1224+
Beetle b = new Beetle();
1225+
}
1226+
}
1227+
```
1228+
1229+
输出:
1230+
1231+
```
1232+
static Insect.x1 initialized
1233+
static Beetle.x2 initialized
1234+
Beetle constructor
1235+
i = 9, j = 0
1236+
Beetle.k initialized
1237+
k = 47
1238+
j = 39
1239+
```
1240+
1241+
当执行 **java Beetle**,首先会试图访问 **Beetle** 类的 `main()` 方法(一个静态方法),加载器启动并找出 **Beetle** 类的编译代码(在名为 **Beetle.class** 的文件中)。在加载过程中,编译器注意到有一个基类,于是继续加载基类。不论是否创建了基类的对象,基类都会被加载。(可以尝试把创建基类对象的代码注释掉证明这点。)
1242+
1243+
如果基类还存在自身的基类,那么第二个基类也将被加载,以此类推。接下来,根基类(例子中根基类是 **Insect**)的 **static** 的初始化开始执行,接着是派生类,以此类推。这点很重要,因为派生类中 **static** 的初始化可能依赖基类成员是否被正确地初始化。
1244+
1245+
至此,必要的类都加载完毕,可以创建对象了。首先,对象中的所有基本类型变量都被置为默认值,对象引用被设为 **null** —— 这是通过将对象内存设为二进制零值一举生成的。接着会调用基类的构造器。本例中是自动调用的,但是你也可以使用 **super** 调用指定的基类构造器(在 **Beetle** 构造器中的第一步操作)。基类构造器和派生类构造器一样以相同的顺序经历相同的过程。当基类构造器完成后,实例变量按文本顺序初始化。最终,构造器的剩余部分被执行。
1246+
11721247
<!-- Summary -->
11731248

11741249
## 本章小结
11751250

1251+
继承和组合都是从已有类型创建新类型。组合将已有类型作为新类型底层实现的一部分,继承复用的是接口。
1252+
1253+
使用继承时,派生类具有基类接口,因此可以向上转型为基类,这对于多态至关重要,在下一章你将看到。
1254+
1255+
尽管在面向对象编程时极力强调继承,但在开始设计时,优先使用组合(或委托),只有当确实需要时再使用继承。组合更具灵活性。另外,通过对成员类型使用继承的技巧,可以在运行时改变成员的类型和行为。因此,可以在运行时改变组合对象的行为。
1256+
1257+
在设计一个系统时,目标是发现或创建一系列类,每个类有特定的用途,而且既不应太大(包括太多功能难以复用),也不应太小(不添加其他功能就无法使用)。如果设计变得过于复杂,通过将现有类拆分为更小的部分而添加更多的对象,通常是有帮助的。
1258+
1259+
当开始设计一个系统时,记住程序开发是一个增量过程,正如人类学习。它依赖实验,你可以尽可能多做分析,然而在项目开始时仍然无法知道所有的答案。如果把项目视作一个有机的,进化着的生命去培养,而不是视为像摩天大楼一样快速见效,就能获得更多的成功和更迅速的反馈。继承和组合正是可以让你执行如此实验的面向对象编程中最基本的两个工具。
11761260

11771261
<!-- 分页 -->
1262+
11781263
<div style="page-break-after: always;"></div>

0 commit comments

Comments
 (0)