# 16.4 改进设计
《设计模式》书内所有方案的组织都围绕“程序进化时会发生什么变化”这个问题展开。对于任何设计来说,这都可能是最重要的一个问题。若根据对这个问题的回答来构造自己的系统,就可以得到两个方面的结果:系统不仅更易维护(而且更廉价),而且能产生一些能够重复使用的对象,进而使其他相关系统的构造也变得更廉价。这正是面向对象程序设计的优势所在,但这一优势并不是自动体现出来的。它要求对我们对需要解决的问题有全面而且深入的理解。在这一节中,我们准备在系统的逐步改进过程中向大家展示如何做到这一点。
就目前这个回收系统来说,对“什么会变化”这个问题的回答是非常普通的:更多的类型会加入系统。因此,设计的目标就是尽可能简化这种类型的添加。在回收程序中,我们准备把涉及特定类型信息的所有地方都封装起来。这样一来(如果没有别的原因),所有变化对那些封装来说都是在本地进行的。这种处理方式也使代码剩余的部分显得特别清爽。
## 16.4.1 “制作更多的对象”
这样便引出了面向对象程序设计时一条常规的准则,我最早是在Grady Booch那里听说的:“若设计过于复杂,就制作更多的对象”。尽管听起来有些暧昧,且简单得可笑,但这确实是我知道的最有用一条准则(大家以后会注意到“制作更多的对象”经常等同于“添加另一个层次的迂回”)。一般情况下,如果发现一个地方充斥着大量繁复的代码,就需要考虑什么类能使它显得清爽一些。用这种方式整理系统,往往会得到一个更好的结构,也使程序更加灵活。
首先考虑Trash对象首次创建的地方,这是`main()`里的一个`switch`语句:
```
for(int i = 0; i < 30; i++)
switch((int)(Math.random() * 3)) {
case 0 :
bin.addElement(new
Aluminum(Math.random() * 100));
break;
case 1 :
bin.addElement(new
Paper(Math.random() * 100));
break;
case 2 :
bin.addElement(new
Glass(Math.random() * 100));
}
```
这些代码显然“过于复杂”,也是新类型加入时必须改动代码的场所之一。如果经常都要加入新类型,那么更好的方案就是建立一个独立的方法,用它获取所有必需的信息,并创建一个引用,指向正确类型的一个对象——已经向上转换到一个`Trash`对象。在《设计模式》中,它被粗略地称呼为“创建模式”。要在这里应用的特殊模式是`Factory`方法的一种变体。在这里,`Factory`方法属于`Trash`的一名`static`(静态)成员。但更常见的一种情况是:它属于派生类中一个被重载的方法。
`Factory`方法的基本原理是我们将创建对象所需的基本信息传递给它,然后返回并等候引用(已经向上转换至基类型)作为返回值出现。从这时开始,就可以按多态性的方式对待对象了。因此,我们根本没必要知道所创建对象的准确类型是什么。事实上,`Factory`方法会把自己隐藏起来,我们是看不见它的。这样做可防止不慎的误用。如果想在没有多态性的前提下使用对象,必须明确地使用RTTI和指定转换。
但仍然存在一个小问题,特别是在基类中使用更复杂的方法(不是在这里展示的那种),且在派生类里重载(覆盖)了它的前提下。如果在派生类里请求的信息要求更多或者不同的参数,那么该怎么办呢?“创建更多的对象”解决了这个问题。为实现`Factory`方法,`Trash`类使用了一个新的方法,名为`factory`。为了将创建数据隐藏起来,我们用一个名为`Info`的新类包含`factory`方法创建适当的`Trash`对象时需要的全部信息。下面是`Info`一种简单的实现方式:
```
class Info {
int type;
// Must change this to add another type:
static final int MAX_NUM = 4;
double data;
Info(int typeNum, double dat) {
type = typeNum % MAX_NUM;
data = dat;
}
}
```
`Info`对象唯一的任务就是容纳用于`factory()`方法的信息。现在,假如出现了一种特殊情况,`factory()`需要更多或者不同的信息来新建一种类型的`Trash`对象,那么再也不需要改动`factory()`了。通过添加新的数据和构造器,我们可以修改`Info`类,或者采用子类处理更典型的面向对象形式。
用于这个简单示例的`factory()`方法如下:
```
static Trash factory(Info i) {
switch(i.type) {
default: // To quiet the compiler
case 0:
return new Aluminum(i.data);
case 1:
return new Paper(i.data);
case 2:
return new Glass(i.data);
// Two lines here:
case 3:
return new Cardboard(i.data);
}
}
```
在这里,对象的准确类型很容易即可判断出来。但我们可以设想一些更复杂的情况,`factory()`将采用一种复杂的算法。无论如何,现在的关键是它已隐藏到某个地方,而且我们在添加新类型时知道去那个地方。
新对象在`main()`中的创建现在变得非常简单和清爽:
```
for(int i = 0; i < 30; i++)
bin.addElement(
Trash.factory(
new Info(
(int)(Math.random() * Info.MAX_NUM),
Math.random() * 100)));
```
我们在这里创建了一个`Info`对象,用于将数据传入`factory()`;后者在内存堆中创建某种T`rash`对象,并返回添加到`Vector bin`内的引用。当然,如果改变了参数的数量及类型,仍然需要修改这个语句。但假如`Info`对象的创建是自动进行的,也可以避免那个麻烦。例如,可将参数的一个`Vector`传递到`Info`对象的构造器中(或直接传入一个`factory()`调用)。这要求在运行期间对参数进行分析与检查,但确实提供了非常高的灵活程度。
大家从这个代码可看出`Factory`要负责解决的“领头变化”问题:如果向系统添加了新类型(发生了变化),唯一需要修改的代码在`Factory`内部,所以`Factory`将那种变化的影响隔离出来了。
## 16.4.2 用于原型创建的一个模式
上述设计模式的一个问题是仍然需要一个中心场所,必须在那里知道所有类型的对象:在`factory()`方法内部。如果经常都要向系统添加新类型,`factory()`方法为每种新类型都要修改一遍。若确实对这个问题感到苦恼,可试试再深入一步,将与类型有关的所有信息——包括它的创建过程——都移入代表那种类型的类内部。这样一来,每次新添一种类型的时候,需要做的唯一事情就是从一个类继承。
为将涉及类型创建的信息移入特定类型的Trash里,必须使用“原型”(`prototype`)模式(来自《设计模式》那本书)。这里最基本的想法是我们有一个主控对象序列,为自己感兴趣的每种类型都制作一个。这个序列中的对象只能用于新对象的创建,采用的操作类似内建到Java根类`Object`内部的`clone()`机制。在这种情况下,我们将克隆方法命名为`tClone()`。准备创建一个新对象时,要事先收集好某种形式的信息,用它建立我们希望的对象类型。然后在主控序列中遍历,将手上的信息与主控序列中原型对象内任何适当的信息作对比。若找到一个符合自己需要的,就克隆它。
采用这种方案,我们不必用硬编码的方式植入任何创建信息。每个对象都知道如何揭示出适当的信息,以及如何对自身进行克隆。所以一种新类型加入系统的时候,`factory()`方法不需要任何改变。
为解决原型的创建问题,一个方法是添加大量方法,用它们支持新对象的创建。但在Java 1.1中,如果拥有指向`Class`对象的一个引用,那么它已经提供了对创建新对象的支持。利用Java 1.1的“反射”(已在第11章介绍)技术,即便我们只有指向`Class`对象的一个引用,亦可正常地调用一个构造器。这对原型问题的解决无疑是个完美的方案。
原型列表将由指向所有想创建的`Class`对象的一个引用列表间接地表示。除此之外,假如原型处理失败,则`factory()`方法会认为由于一个特定的`Class`对象不在列表中,所以会尝试装载它。通过以这种方式动态装载原型,`Trash`类根本不需要知道自己要操纵的是什么类型。因此,在我们添加新类型时不需要作出任何形式的修改。于是,我们可在本章剩余的部分方便地重复利用它。
```
//: Trash.java
// Base class for Trash recycling examples
package c16.trash;
import java.util.*;
import java.lang.reflect.*;
public abstract class Trash {
private double weight;
Trash(double wt) { weight = wt; }
Trash() {}
public abstract double value();
public double weight() { return weight; }
// Sums the value of Trash in a bin:
public static void sumValue(Vector bin) {
Enumeration e = bin.elements();
double val = 0.0f;
while(e.hasMoreElements()) {
// One kind of RTTI:
// A dynamically-checked cast
Trash t = (Trash)e.nextElement();
val += t.weight() * t.value();
System.out.println(
"weight of " +
// Using RTTI to get type
// information about the class:
t.getClass().getName() +
" = " + t.weight());
}
System.out.println("Total value = " + val);
}
// Remainder of class provides support for
// prototyping:
public static class PrototypeNotFoundException
extends Exception {}
public static class CannotCreateTrashException
extends Exception {}
private static Vector trashTypes =
new Vector();
public static Trash factory(Info info)
throws PrototypeNotFoundException,
CannotCreateTrashException {
for(int i = 0; i < trashTypes.size(); i++) {
// Somehow determine the new type
// to create, and create one:
Class tc =
(Class)trashTypes.elementAt(i);
if (tc.getName().indexOf(info.id) != -1) {
try {
// Get the dynamic constructor method
// that takes a double argument:
Constructor ctor =
tc.getConstructor(
new Class[] {double.class});
// Call the constructor to create a
// new object:
return (Trash)ctor.newInstance(
new Object[]{new Double(info.data)});
} catch(Exception ex) {
ex.printStackTrace();
throw new CannotCreateTrashException();
}
}
}
// Class was not in the list. Try to load it,
// but it must be in your class path!
try {
System.out.println("Loading " + info.id);
trashTypes.addElement(
Class.forName(info.id));
} catch(Exception e) {
e.printStackTrace();
throw new PrototypeNotFoundException();
}
// Loaded successfully. Recursive call
// should work this time:
return factory(info);
}
public static class Info {
public String id;
public double data;
public Info(String name, double data) {
id = name;
this.data = data;
}
}
} ///:~
```
基本`Trash`类和`sumValue()`还是象往常一样。这个类剩下的部分支持原型模式。大家首先会看到两个内部类(被设为`static`属性,使其成为只为代码组织目的而存在的内部类),它们描述了可能出现的异常。在它后面跟随的是一个`Vector trashTypes`,用于容纳`Class`引用。
在`Trash.factory()`中,`Info`对象`id`(`Info`类的另一个版本,与前面讨论的不同)内部的`String`包含了要创建的那种`Trash`的类型名称。这个`String`会与列表中的`Class`名比较。若存在相符的,那便是要创建的对象。当然,还有很多方法可以决定我们想创建的对象。之所以要采用这种方法,是因为从一个文件读入的信息可以转换成对象。
发现自己要创建的`Trash`(垃圾)种类后,接下来就轮到“反射”方法大显身手了。`getConstructor()`方法需要取得自己的参数——由`Class`引用构成的一个数组。这个数组代表着不同的参数,并按它们正确的顺序排列,以便我们查找的构造器使用。在这儿,该数组是用Java 1.1的数组创建语法动态创建的:
```
new Class[] {double.class}
```
这个代码假定所有`Trash`类型都有一个需要`double`数值的构造器(注意`double.class`与`Double.class`是不同的)。若考虑一种更灵活的方案,亦可调用`getConstructors()`,令其返回可用构造器的一个数组。
从`getConstructors()`返回的是指向一个`Constructor`对象的引用(该对象是`java.lang.reflect`的一部分)。我们用方法`newInstance()`动态地调用构造器。该方法需要获取包含了实际参数的一个`Object`数组。这个数组同样是按Java 1.1的语法创建的:
```
new Object[] {new Double(info.data)}
```
在这种情况下,`double`必须置入一个封装(容器)类的内部,使其真正成为这个对象数组的一部分。通过调用`newInstance()`,会提取出`double`,但大家可能会觉得稍微有些迷惑——参数既可能是`double`,也可能是`Double`,但在调用的时候必须用`Double`传递。幸运的是,这个问题只存在于基本数据类型中间。
理解了具体的过程后,再来创建一个新对象,并且只为它提供一个`Class`引用,事情就变得非常简单了。就目前的情况来说,内部循环中的`return`永远不会执行,我们在终点就会退出。在这儿,程序动态装载`Class`对象,并把它加入`trashTypes`(垃圾类型)列表,从而试图纠正这个问题。若仍然找不到真正有问题的地方,同时装载又是成功的,那么就重复调用`factory`方法,重新试一遍。
正如大家会看到的那样,这种设计模式最大的优点就是不需要改动代码。无论在什么情况下,它都能正常地使用(假定所有`Trash`子类都包含了一个构造器,用以获取单个`double`参数)。
(1) Trash子类
为了与原型机制相适应,对`Trash`每个新子类唯一的要求就是在其中包含了一个构造器,指示它获取一个`double`参数。Java 1.1的“反射”机制可负责剩下的所有工作。
下面是不同类型的`Trash`,每种类型都有它们自己的文件里,但都属于`Trash`包的一部分(同样地,为了方便在本章内重复使用):
```
//: Aluminum.java
// The Aluminum class with prototyping
package c16.trash;
public class Aluminum extends Trash {
private static double val = 1.67f;
public Aluminum(double wt) { super(wt); }
public double value() { return val; }
public static void value(double newVal) {
val = newVal;
}
} ///:~
```
下面是一种新的`Trash`类型:
```
//: Cardboard.java
// The Cardboard class with prototyping
package c16.trash;
public class Cardboard extends Trash {
private static double val = 0.23f;
public Cardboard(double wt) { super(wt); }
public double value() { return val; }
public static void value(double newVal) {
val = newVal;
}
} ///:~
```
可以看出,除构造器以外,这些类根本没有什么特别的地方。
(2) 从外部文件中解析出`Trash`
与`Trash`对象有关的信息将从一个外部文件中读取。针对`Trash`的每个方面,文件内列出了所有必要的信息——每行都代表一个方面,采用`垃圾(废品)名称:值`的固定格式。例如:
```
c16.Trash.Glass:54
c16.Trash.Paper:22
c16.Trash.Paper:11
c16.Trash.Glass:17
c16.Trash.Aluminum:89
c16.Trash.Paper:88
c16.Trash.Aluminum:76
c16.Trash.Cardboard:96
c16.Trash.Aluminum:25
c16.Trash.Aluminum:34
c16.Trash.Glass:11
c16.Trash.Glass:68
c16.Trash.Glass:43
c16.Trash.Aluminum:27
c16.Trash.Cardboard:44
c16.Trash.Aluminum:18
c16.Trash.Paper:91
c16.Trash.Glass:63
c16.Trash.Glass:50
c16.Trash.Glass:80
c16.Trash.Aluminum:81
c16.Trash.Cardboard:12
c16.Trash.Glass:12
c16.Trash.Glass:54
c16.Trash.Aluminum:36
c16.Trash.Aluminum:93
c16.Trash.Glass:93
c16.Trash.Paper:80
c16.Trash.Glass:36
c16.Trash.Glass:12
c16.Trash.Glass:60
c16.Trash.Paper:66
c16.Trash.Aluminum:36
c16.Trash.Cardboard:22
```
注意在给定类名的时候,类路径必须包含在内,否则就找不到类。
为解析它,每一行内容都会读入,并用字符串方法`indexOf()`来建立`:`的一个索引。首先用字符串方法`substring()`取出垃圾的类型名称,接着用一个静态方法`Double.valueOf()`取得相应的值,并转换成一个`double`值。`trim()`方法则用于删除字符串两头的多余空格。
`Trash`解析器置入单独的文件中,因为本章将不断地用到它。如下所示:
```
//: ParseTrash.java
// Open a file and parse its contents into
// Trash objects, placing each into a Vector
package c16.trash;
import java.util.*;
import java.io.*;
public class ParseTrash {
public static void
fillBin(String filename, Fillable bin) {
try {
BufferedReader data =
new BufferedReader(
new FileReader(filename));
String buf;
while((buf = data.readLine())!= null) {
String type = buf.substring(0,
buf.indexOf(':')).trim();
double weight = Double.valueOf(
buf.substring(buf.indexOf(':') + 1)
.trim()).doubleValue();
bin.addTrash(
Trash.factory(
new Trash.Info(type, weight)));
}
data.close();
} catch(IOException e) {
e.printStackTrace();
} catch(Exception e) {
e.printStackTrace();
}
}
// Special case to handle Vector:
public static void
fillBin(String filename, Vector bin) {
fillBin(filename, new FillableVector(bin));
}
} ///:~
```
在`RecycleA.java`中,我们用一个`Vector`容纳`Trash`对象。然而,亦可考虑采用其他集合类型。为做到这一点,`fillBin()`的第一个版本将获取指向一个`Fillable`的引用。后者是一个接口,用于支持一个名为`addTrash()`的方法:
```
//: Fillable.java
// Any object that can be filled with Trash
package c16.trash;
public interface Fillable {
void addTrash(Trash t);
} ///:~
```
支持该接口的所有东西都能伴随`fillBin`使用。当然,`Vector`并未实现`Fillable`,所以它不能工作。由于`Vector`将在大多数例子中应用,所以最好的做法是添加另一个重载的`fillBin()`方法,令其以一个`Vector`作为参数。利用一个适配器(`Adapter`)类,这个`Vector`可作为一个`Fillable`对象使用:
```
//: FillableVector.java
// Adapter that makes a Vector Fillable
package c16.trash;
import java.util.*;
public class FillableVector implements Fillable {
private Vector v;
public FillableVector(Vector vv) { v = vv; }
public void addTrash(Trash t) {
v.addElement(t);
}
} ///:~
```
可以看到,这个类唯一的任务就是负责将`Fillable`的`addTrash()`同`Vector`的`addElement()`方法连接起来。利用这个类,已重载的`fillBin()`方法可在`ParseTrash.java`中伴随一个`Vector`使用:
```
public static void
fillBin(String filename, Vector bin) {
fillBin(filename, new FillableVector(bin));
}
```
这种方案适用于任何频繁用到的集合类。除此以外,集合类还可提供它自己的适配器类,并实现`Fillable`(稍后即可看到,在`DynaTrash.java`中)。
(3) 原型机制的重复应用
现在,大家可以看到采用原型技术的、修订过的`RecycleA.java`版本了:
```
//: RecycleAP.java
// Recycling with RTTI and Prototypes
package c16.recycleap;
import c16.trash.*;
import java.util.*;
public class RecycleAP {
public static void main(String[] args) {
Vector bin = new Vector();
// Fill up the Trash bin:
ParseTrash.fillBin("Trash.dat", bin);
Vector
glassBin = new Vector(),
paperBin = new Vector(),
alBin = new Vector();
Enumeration sorter = bin.elements();
// Sort the Trash:
while(sorter.hasMoreElements()) {
Object t = sorter.nextElement();
// RTTI to show class membership:
if(t instanceof Aluminum)
alBin.addElement(t);
if(t instanceof Paper)
paperBin.addElement(t);
if(t instanceof Glass)
glassBin.addElement(t);
}
Trash.sumValue(alBin);
Trash.sumValue(paperBin);
Trash.sumValue(glassBin);
Trash.sumValue(bin);
}
} ///:~
```
所有`Trash`对象——以及`ParseTrash`及支撑类——现在都成为名为`c16.trash`的一个包的一部分,所以它们可以简单地导入。
无论打开包含了`Trash`描述信息的数据文件,还是对那个文件进行解析,所有涉及到的操作均已封装到`static`(静态)方法`ParseTrash.fillBin()`里。所以它现在已经不是我们设计过程中要注意的一个重点。在本章剩余的部分,大家经常都会看到无论添加的是什么类型的新类,`ParseTrash.fillBin()`都会持续工作,不会发生改变,这无疑是一种优良的设计模式。
提到对象的创建,这一方案确实已将新类型加入系统所需的变动严格地“本地化”了。但在使用RTTI的过程中,却存在着一个严重的问题,这里已明确地显露出来。程序表面上工作得很好,但却永远侦测到不能“硬纸板”(`Cardboard`)这种新的废品类型——即使列表里确实有一个硬纸板类型!之所以会出现这种情况,完全是由于使用了RTTI的缘故。RTTI只会查找那些我们告诉它查找的东西。RTTI在这里错误的用法是“系统中的每种类型”都进行了测试,而不是仅测试一种类型或者一个类型子集。正如大家以后会看到的那样,在测试每一种类型时可换用其他方式来运用多态性特征。但假如以这种形式过多地使用RTTI,而且又在自己的系统里添加了一种新类型,很容易就会忘记在程序里作出适当的改动,从而埋下以后难以发现的Bug。因此,在这种情况下避免使用RTTI是很有必要的,这并不仅仅是为了表面好看——也是为了产生更易维护的代码。
- 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 推荐读物