# 8.4 集合的类型
标准Java 1.0和1.1库配套提供了非常少的一系列集合类。但对于自己的大多数编程要求,它们基本上都能胜任。正如大家到本章末尾会看到的,Java 1.2提供的是一套重新设计过的大型集合库。
## 8.4.1 `Vector`
`Vector`的用法很简单,这已在前面的例子中得到了证明。尽管我们大多数时候只需用`addElement()`插入对象,用`elementAt()`一次提取一个对象,并用`elements()`获得对序列的一个“枚举”。但仍有其他一系列方法是非常有用的。同我们对于Java库惯常的做法一样,在这里并不使用或讲述所有这些方法。但请务必阅读相应的电子文档,对它们的工作有一个大概的认识。
(1) 崩溃Java
Java标准集合里包含了`toString()`方法,所以它们能生成自己的`String`表达方式,包括它们容纳的对象。例如在`Vector`中,`toString()`会在`Vector`的各个元素中步进和遍历,并为每个元素调用`toString()`。假定我们现在想打印出自己类的地址。看起来似乎简单地引用`this`即可(特别是C++程序员有这样做的倾向):
```
//: CrashJava.java
// One way to crash Java
import java.util.*;
public class CrashJava {
public String toString() {
return "CrashJava address: " + this + "\n";
}
public static void main(String[] args) {
Vector v = new Vector();
for(int i = 0; i < 10; i++)
v.addElement(new CrashJava());
System.out.println(v);
}
} ///:~
```
若只是简单地创建一个`CrashJava`对象,并将其打印出来,就会得到无穷无尽的一系列异常错误。然而,假如将`CrashJava`对象置入一个`Vector`,并象这里演示的那样打印`Vector`,就不会出现什么错误提示,甚至连一个异常都不会出现。此时Java只是简单地崩溃(但至少它没有崩溃我的操作系统)。这已在Java 1.1中测试通过。
此时发生的是字符串的自动类型转换。当我们使用下述语句时:
```
"CrashJava address: " + this
```
编译器就在一个字符串后面发现了一个`+`以及好象并非字符串的其他东西,所以它会试图将`this`转换成一个字符串。转换时调用的是`toString()`,后者会产生一个递归调用。若在一个`Vector`内出现这种事情,看起来栈就会溢出,同时异常控制机制根本没有机会作出响应。
若确实想在这种情况下打印出对象的地址,解决方案就是调用`Object`的`toString`方法。此时就不必加入`this`,只需使用`super.toString()`。当然,采取这种做法也有一个前提:我们必须从`Object`直接继承,或者没有一个父类覆盖了`toString`方法。
## 8.4.2 `BitSet`
`BitSet`实际是由“二进制位”构成的一个`Vector`。如果希望高效率地保存大量“开-关”信息,就应使用`BitSet`。它只有从尺寸的角度看才有意义;如果希望的高效率的访问,那么它的速度会比使用一些固有类型的数组慢一些。
此外,`BitSet`的最小长度是一个长整数(`Long`)的长度:64位。这意味着假如我们准备保存比这更小的数据,如8位数据,那么`BitSet`就显得浪费了。所以最好创建自己的类,用它容纳自己的标志位。
在一个普通的`Vector`中,随我们加入越来越多的元素,集合也会自我膨胀。在某种程度上,`BitSet`也不例外。也就是说,它有时会自行扩展,有时则不然。而且Java的1.0版本似乎在这方面做得最糟,它的`BitSet`表现十分差强人意(Java1.1已改正了这个问题)。下面这个例子展示了`BitSet`是如何运作的,同时演示了1.0版本的错误:
```
//: Bits.java
// Demonstration of BitSet
import java.util.*;
public class Bits {
public static void main(String[] args) {
Random rand = new Random();
// Take the LSB of nextInt():
byte bt = (byte)rand.nextInt();
BitSet bb = new BitSet();
for(int i = 7; i >=0; i--)
if(((1 << i) & bt) != 0)
bb.set(i);
else
bb.clear(i);
System.out.println("byte value: " + bt);
printBitSet(bb);
short st = (short)rand.nextInt();
BitSet bs = new BitSet();
for(int i = 15; i >=0; i--)
if(((1 << i) & st) != 0)
bs.set(i);
else
bs.clear(i);
System.out.println("short value: " + st);
printBitSet(bs);
int it = rand.nextInt();
BitSet bi = new BitSet();
for(int i = 31; i >=0; i--)
if(((1 << i) & it) != 0)
bi.set(i);
else
bi.clear(i);
System.out.println("int value: " + it);
printBitSet(bi);
// Test bitsets >= 64 bits:
BitSet b127 = new BitSet();
b127.set(127);
System.out.println("set bit 127: " + b127);
BitSet b255 = new BitSet(65);
b255.set(255);
System.out.println("set bit 255: " + b255);
BitSet b1023 = new BitSet(512);
// Without the following, an exception is thrown
// in the Java 1.0 implementation of BitSet:
// b1023.set(1023);
b1023.set(1024);
System.out.println("set bit 1023: " + b1023);
}
static void printBitSet(BitSet b) {
System.out.println("bits: " + b);
String bbits = new String();
for(int j = 0; j < b.size() ; j++)
bbits += (b.get(j) ? "1" : "0");
System.out.println("bit pattern: " + bbits);
}
} ///:~
```
随机数字生成器用于创建一个随机的`byte`、`short`和`int`。每一个都会转换成`BitSet`内相应的位模型。此时一切都很正常,因为`BitSet`是64位的,所以它们都不会造成最终尺寸的增大。但在Java 1.0中,一旦`BitSet`大于64位,就会出现一些令人迷惑不解的行为。假如我们设置一个只比`BitSet`当前分配存储空间大出1的一个位,它能够正常地扩展。但一旦试图在更高的位置设置位,同时不先接触边界,就会得到一个恼人的异常。这正是由于`BitSet`在Java 1.0里不能正确扩展造成的。本例创建了一个512位的`BitSet`。构造器分配的存储空间是位数的两倍。所以假如设置位1024或更高的位,同时没有先设置位1023,就会在Java 1.0里得到一个异常。但幸运的是,这个问题已在Java 1.1得到了改正。所以如果是为Java 1.0写代码,请尽量避免使用`BitSet`。
## 8.4.3 `Stack`
`Stack`有时也可以称为“后入先出”(LIFO)集合。换言之,我们在栈里最后“压入”的东西将是以后第一个“弹出”的。和其他所有Java集合一样,我们压入和弹出的都是“对象”,所以必须对自己弹出的东西进行“转换”。
一种很少见的做法是拒绝使用`Vector`作为一个`Stack`的基本构成元素,而是从`Vector`里“继承”一个`Stack`。这样一来,它就拥有了一个`Vector`的所有特征及行为,另外加上一些额外的`Stack`行为。很难判断出设计者到底是明确想这样做,还是属于一种固有的设计。
下面是一个简单的栈示例,它能读入数组的每一行,同时将其作为字符串压入栈。
```
//: Stacks.java
// Demonstration of Stack Class
import java.util.*;
public class Stacks {
static String[] months = {
"January", "February", "March", "April",
"May", "June", "July", "August", "September",
"October", "November", "December" };
public static void main(String[] args) {
Stack stk = new Stack();
for(int i = 0; i < months.length; i++)
stk.push(months[i] + " ");
System.out.println("stk = " + stk);
// Treating a stack as a Vector:
stk.addElement("The last line");
System.out.println(
"element 5 = " + stk.elementAt(5));
System.out.println("popping elements:");
while(!stk.empty())
System.out.println(stk.pop());
}
} ///:~
```
`months`数组的每一行都通过`push()`继承进入栈,稍后用`pop()`从栈的顶部将其取出。要声明的一点是,`Vector`操作亦可针对Stack对象进行。这可能是由继承的特质决定的——`Stack`“属于”一种`Vector`。因此,能对`Vector`进行的操作亦可针对`Stack`进行,例如`elementAt()`方法。
## 8.4.4 `Hashtable`
`Vector`允许我们用一个数字从一系列对象中作出选择,所以它实际是将数字同对象关联起来了。但假如我们想根据其他标准选择一系列对象呢?栈就是这样的一个例子:它的选择标准是“最后压入栈的东西”。这种“从一系列对象中选择”的概念亦可叫作一个“映射”、“字典”或者“关联数组”。从概念上讲,它看起来象一个`Vector`,但却不是通过数字来查找对象,而是用另一个对象来查找它们!这通常都属于一个程序中的重要进程。
在Java中,这个概念具体反映到抽象类`Dictionary`身上。该类的接口是非常直观的`size()`告诉我们其中包含了多少元素;`isEmpty()`判断是否包含了元素(是则为`true`);`put(Object key, Object value)`添加一个值(我们希望的东西),并将其同一个键关联起来(想用于搜索它的东西);`get(Object key)`获得与某个键对应的值;而`remove(Object Key)`用于从列表中删除“键-值”对。还可以使用枚举技术:`keys()`产生对键的一个枚举(`Enumeration`);而`elements()`产生对所有值的一个枚举。这便是一个`Dictionary`(字典)的全部。
`Dictionary`的实现过程并不麻烦。下面列出一种简单的方法,它使用了两个`Vector`,一个用于容纳键,另一个用来容纳值:
```
//: AssocArray.java
// Simple version of a Dictionary
import java.util.*;
public class AssocArray extends Dictionary {
private Vector keys = new Vector();
private Vector values = new Vector();
public int size() { return keys.size(); }
public boolean isEmpty() {
return keys.isEmpty();
}
public Object put(Object key, Object value) {
keys.addElement(key);
values.addElement(value);
return key;
}
public Object get(Object key) {
int index = keys.indexOf(key);
// indexOf() Returns -1 if key not found:
if(index == -1) return null;
return values.elementAt(index);
}
public Object remove(Object key) {
int index = keys.indexOf(key);
if(index == -1) return null;
keys.removeElementAt(index);
Object returnval = values.elementAt(index);
values.removeElementAt(index);
return returnval;
}
public Enumeration keys() {
return keys.elements();
}
public Enumeration elements() {
return values.elements();
}
// Test it:
public static void main(String[] args) {
AssocArray aa = new AssocArray();
for(char c = 'a'; c <= 'z'; c++)
aa.put(String.valueOf(c),
String.valueOf(c)
.toUpperCase());
char[] ca = { 'a', 'e', 'i', 'o', 'u' };
for(int i = 0; i < ca.length; i++)
System.out.println("Uppercase: " +
aa.get(String.valueOf(ca[i])));
}
} ///:~
```
在对`AssocArray`的定义中,我们注意到的第一个问题是它“扩展”了字典。这意味着`AssocArray`属于`Dictionary`的一种类型,所以可对其发出与`Dictionary`一样的请求。如果想生成自己的`Dictionary`,而且就在这里进行,那么要做的全部事情只是填充位于`Dictionar`y内的所有方法(而且必须覆盖所有方法,因为它们——除构造器外——都是抽象的)。
`Vector key`和`value`通过一个标准索引编号链接起来。也就是说,如果用`roof`的一个键以及`blue`的一个值调用`put()`——假定我们准备将一个房子的各部分与它们的油漆颜色关联起来,而且`AssocArray`里已有100个元素,那么`roof`就会有101个键元素,而`blue`有101个值元素。而且要注意一下`get()`,假如我们作为键传递`roof`,它就会产生与`keys.index.Of()`的索引编号,然后用那个索引编号生成相关的值向量内的值。
`main()`中进行的测试是非常简单的;它只是将小写字符转换成大写字符,这显然可用更有效的方式进行。但它向我们揭示出了`AssocArray`的强大功能。
标准Java库只包含`Dictionary`的一个变种,名为`Hashtable`(散列表,注释③)。Java的散列表具有与`AssocArray`相同的接口(因为两者都是从`Dictionary`继承来的)。但有一个方面却反映出了差别:执行效率。若仔细想想必须为一个`get()`做的事情,就会发现在一个`Vector`里搜索键的速度要慢得多。但此时用散列表却可以加快不少速度。不必用冗长的线性搜索技术来查找一个键,而是用一个特殊的值,名为“散列码”。散列码可以获取对象中的信息,然后将其转换成那个对象“相对唯一”的整数(`int`)。所有对象都有一个散列码,而`hashCode()`是根类`Object`的一个方法。`Hashtable`获取对象的`hashCode()`,然后用它快速查找键。这样可使性能得到大幅度提升(④)。散列表的具体工作原理已超出了本书的范围(⑤)——大家只需要知道散列表是一种快速的“字典”(`Dictionary`)即可,而字典是一种非常有用的工具。
③:如计划使用RMI(在第15章详述),应注意将远程对象置入散列表时会遇到一个问题(参阅《Core Java》,作者Conrell和Horstmann,Prentice-Hall 1997年出版)
④:如这种速度的提升仍然不能满足你对性能的要求,甚至可以编写自己的散列表例程,从而进一步加快表格的检索过程。这样做可避免在与`Object`之间进行转换的时间延误,也可以避开由Java类库散列表例程内建的同步过程。
⑤:我的知道的最佳参考读物是《Practical Algorithms for Programmers》,作者为Andrew Binstock和John Rex,Addison-Wesley 1995年出版。
作为应用散列表的一个例子,可考虑用一个程序来检验Java的`Math.random()`方法的随机性到底如何。在理想情况下,它应该产生一系列完美的随机分布数字。但为了验证这一点,我们需要生成数量众多的随机数字,然后计算落在不同范围内的数字多少。散列表可以极大简化这一工作,因为它能将对象同对象关联起来(此时是将`Math.random()`生成的值同那些值出现的次数关联起来)。如下所示:
```
//: Statistics.java
// Simple demonstration of Hashtable
import java.util.*;
class Counter {
int i = 1;
public String toString() {
return Integer.toString(i);
}
}
class Statistics {
public static void main(String[] args) {
Hashtable ht = new Hashtable();
for(int i = 0; i < 10000; i++) {
// Produce a number between 0 and 20:
Integer r =
new Integer((int)(Math.random() * 20));
if(ht.containsKey(r))
((Counter)ht.get(r)).i++;
else
ht.put(r, new Counter());
}
System.out.println(ht);
}
} ///:~
```
在`main()`中,每次产生一个随机数字,它都会封装到一个`Integer`对象里,使引用能够随同散列表一起使用(不可对一个集合使用基本数据类型,只能使用对象引用)。`containKey()`方法检查这个键是否已经在集合里(也就是说,那个数字以前发现过吗?)若已在集合里,则`get()`方法获得那个键关联的值,此时是一个`Counter`(计数器)对象。计数器内的值`i`随后会增加1,表明这个特定的随机数字又出现了一次。
假如键以前尚未发现过,那么方法`put()`仍然会在散列表内置入一个新的“键-值”对。在创建之初,`Counter`会自己的变量`i`自动初始化为1,它标志着该随机数字的第一次出现。
为显示散列表,只需把它简单地打印出来即可。`Hashtable toString()`方法能遍历所有键-值对,并为每一对都调用`toString()`。`Integer toString()`是事先定义好的,可看到计数器使用的`toString`。一次运行的结果(添加了一些换行)如下:
```
{19=526, 18=533, 17=460, 16=513, 15=521, 14=495,
13=512, 12=483, 11=488, 10=487, 9=514, 8=523,
7=497, 6=487, 5=480, 4=489, 3=509, 2=503, 1=475,
0=505}
```
大家或许会对`Counter`类是否必要感到疑惑,它看起来似乎根本没有封装类`Integer`的功能。为什么不用`int`或`Integer`呢?事实上,由于所有集合能容纳的仅有对象引用,所以根本不可以使用整数。学过集合后,封装类的概念对大家来说就可能更容易理解了,因为不可以将任何基本数据类型置入集合里。然而,我们对Java包装器能做的唯一事情就是将其初始化成一个特定的值,然后读取那个值。也就是说,一旦包装器对象已经创建,就没有办法改变一个值。这使得`Integer`包装器对解决我们的问题毫无意义,所以不得不创建一个新类,用它来满足自己的要求。
(1) 创建“关键”类
在前面的例子里,我们用一个标准库的类(`Integer`)作为`Hashtable`的一个键使用。作为一个键,它能很好地工作,因为它已经具备正确运行的所有条件。但在使用散列表的时候,一旦我们创建自己的类作为键使用,就会遇到一个很常见的问题。例如,假设一套天气预报系统将`Groundhog`(土拔鼠)对象匹配成`Prediction`(预报)。这看起来非常直观:我们创建两个类,然后将`Groundhog`作为键使用,而将`Prediction`作为值使用。如下所示:
```
//: SpringDetector.java
// Looks plausible, but doesn't work right.
import java.util.*;
class Groundhog {
int ghNumber;
Groundhog(int n) { ghNumber = n; }
}
class Prediction {
boolean shadow = Math.random() > 0.5;
public String toString() {
if(shadow)
return "Six more weeks of Winter!";
else
return "Early Spring!";
}
}
public class SpringDetector {
public static void main(String[] args) {
Hashtable ht = new Hashtable();
for(int i = 0; i < 10; i++)
ht.put(new Groundhog(i), new Prediction());
System.out.println("ht = " + ht + "\n");
System.out.println(
"Looking up prediction for groundhog #3:");
Groundhog gh = new Groundhog(3);
if(ht.containsKey(gh))
System.out.println((Prediction)ht.get(gh));
}
} ///:~
```
每个`Groundhog`都具有一个标识号码,所以赤了在散列表中查找一个`Prediction`,只需指示它“告诉我与`Groundhog`号码3相关的`Prediction`”。`Prediction`类包含了一个布尔值,用`Math.random()`进行初始化,以及一个`toString()`为我们解释结果。在`main()`中,用`Groundhog`以及与它们相关的`Prediction`填充一个散列表。散列表被打印出来,以便我们看到它们确实已被填充。随后,用标识号码为3的一个`Groundhog`查找与`Groundhog #3`对应的预报。
看起来似乎非常简单,但实际是不可行的。问题在于`Groundhog`是从通用的`Object`根类继承的(若当初未指定基类,则所有类最终都是从`Object`继承的)。事实上是用`Object`的`hashCode()`方法生成每个对象的散列码,而且默认情况下只使用它的对象的地址。所以,`Groundhog(3)`的第一个实例并不会产生与`Groundhog(3)`第二个实例相等的散列码,而我们用第二个实例进行检索。
大家或许认为此时要做的全部事情就是正确地覆盖`hashCode()`。但这样做依然行不能,除非再做另一件事情:覆盖也属于`Object`一部分的`equals()`。当散列表试图判断我们的键是否等于表内的某个键时,就会用到这个方法。同样地,默认的`Object.equals()`只是简单地比较对象地址,所以一个`Groundhog(3)`并不等于另一个`Groundhog(3)`。
因此,为了在散列表中将自己的类作为键使用,必须同时覆盖`hashCode()`和`equals()`,就象下面展示的那样:
```
//: SpringDetector2.java
// If you create a class that's used as a key in
// a Hashtable, you must override hashCode()
// and equals().
import java.util.*;
class Groundhog2 {
int ghNumber;
Groundhog2(int n) { ghNumber = n; }
public int hashCode() { return ghNumber; }
public boolean equals(Object o) {
return (o instanceof Groundhog2)
&& (ghNumber == ((Groundhog2)o).ghNumber);
}
}
public class SpringDetector2 {
public static void main(String[] args) {
Hashtable ht = new Hashtable();
for(int i = 0; i < 10; i++)
ht.put(new Groundhog2(i),new Prediction());
System.out.println("ht = " + ht + "\n");
System.out.println(
"Looking up prediction for groundhog #3:");
Groundhog2 gh = new Groundhog2(3);
if(ht.containsKey(gh))
System.out.println((Prediction)ht.get(gh));
}
} ///:~
```
注意这段代码使用了来自前一个例子的`Prediction`,所以`SpringDetector.java`必须首先编译,否则就会在试图编译`SpringDetector2.java`时得到一个编译期错误。
`Groundhog2.hashCode()`将土拔鼠号码作为一个标识符返回(在这个例子中,程序员需要保证没有两个土拔鼠用同样的ID号码并存)。为了返回一个独一无二的标识符,并不需要`hashCode()`,`equals()`方法必须能够严格判断两个对象是否相等。
`equals()`方法要进行两种检查:检查对象是否为`null`;若不为`null`,则继续检查是否为`Groundhog2`的一个实例(要用到`instanceof`关键字,第11章会详加论述)。即使为了继续执行`equals()`,它也应该是一个`Groundhog2`。正如大家看到的那样,这种比较建立在实际`ghNumber`的基础上。这一次一旦我们运行程序,就会看到它终于产生了正确的输出(许多Java库的类都覆盖了`hashcode()`和`equals()`方法,以便与自己提供的内容适应)。
(2) 属性:`Hashtable`的一种类型
在本书的第一个例子中,我们使用了一个名为`Properties`(属性)的`Hashtable`类型。在那个例子中,下述程序行:
```
Properties p = System.getProperties();
p.list(System.out);
```
调用了一个名为`getProperties()`的`static`方法,用于获得一个特殊的`Properties`对象,对系统的某些特征进行描述。`list()`属于`Properties`的一个方法,可将内容发给我们选择的任何流式输出。也有一个`save()`方法,可用它将属性列表写入一个文件,以便日后用`load()`方法读取。
尽管`Properties`类是从`Hashtable`继承的,但它也包含了一个散列表,用于容纳“默认”属性的列表。所以假如没有在主列表里找到一个属性,就会自动搜索默认属性。
`Properties`类亦可在我们的程序中使用(第17章的`ClassScanner.java`便是一例)。在Java库的用户文档中,往往可以找到更多、更详细的说明。
## 8.4.5 再论枚举器
我们现在可以开始演示`Enumeration`(枚举)的真正威力:将穿越一个序列的操作与那个序列的基础结构分隔开。在下面的例子里,`PrintData`类用一个`Enumeration`在一个序列中移动,并为每个对象都调用`toString()`方法。此时创建了两个不同类型的集合:一个`Vector`和一个`Hashtable`。并且在它们里面分别填充`Mouse`和`Hamster`对象(本章早些时候已定义了这些类;注意必须先编译`HamsterMaze.java`和`WorksAnyway.java`,否则下面的程序不能编译)。由于`Enumeration`隐藏了基层集合的结构,所以`PrintData`不知道或者不关心`Enumeration`来自于什么类型的集合:
```
//: Enumerators2.java
// Revisiting Enumerations
import java.util.*;
class PrintData {
static void print(Enumeration e) {
while(e.hasMoreElements())
System.out.println(
e.nextElement().toString());
}
}
class Enumerators2 {
public static void main(String[] args) {
Vector v = new Vector();
for(int i = 0; i < 5; i++)
v.addElement(new Mouse(i));
Hashtable h = new Hashtable();
for(int i = 0; i < 5; i++)
h.put(new Integer(i), new Hamster(i));
System.out.println("Vector");
PrintData.print(v.elements());
System.out.println("Hashtable");
PrintData.print(h.elements());
}
} ///:~
```
注意`PrintData.print()`利用了这些集合中的对象属于`Object`类这一事实,所以它调用了`toString()`。但在解决自己的实际问题时,经常都要保证自己的`Enumeration`穿越某种特定类型的集合。例如,可能要求集合中的所有元素都是一个Shape(几何形状),并含有`draw()`方法。若出现这种情况,必须从`Enumeration.nextElement()`返回的`Object`进行向下转换,以便产生一个`Shape`。
- 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 推荐读物