提交 fae1adc6 编写于 作者: W wizardforcel

ch8

上级 eaeb0d97
......@@ -2,9 +2,9 @@
标准Java 1.0和1.1库配套提供了非常少的一系列集合类。但对于自己的大多数编程要求,它们基本上都能胜任。正如大家到本章末尾会看到的,Java 1.2提供的是一套重新设计过的大型集合库。
8.4.1 Vector
8.4.1 `Vector`
Vector的用法很简单,这已在前面的例子中得到了证明。尽管我们大多数时候只需用`addElement()`插入对象,用`elementAt()`一次提取一个对象,并用`elements()`获得对序列的一个“枚举”。但仍有其他一系列方法是非常有用的。同我们对于Java库惯常的做法一样,在这里并不使用或讲述所有这些方法。但请务必阅读相应的电子文档,对它们的工作有一个大概的认识。
`Vector`的用法很简单,这已在前面的例子中得到了证明。尽管我们大多数时候只需用`addElement()`插入对象,用`elementAt()`一次提取一个对象,并用`elements()`获得对序列的一个“枚举”。但仍有其他一系列方法是非常有用的。同我们对于Java库惯常的做法一样,在这里并不使用或讲述所有这些方法。但请务必阅读相应的电子文档,对它们的工作有一个大概的认识。
1. 崩溃Java
......
# 8.5 排序
Java 1.0和1.1库都缺少的一样东西是算术运算,甚至没有最简单的排序运算方法。因此,我们最好创建一个Vector,利用经典的Quicksort(快速排序)方法对其自身进行排序。
Java 1.0和1.1库都缺少的一样东西是算术运算,甚至没有最简单的排序运算方法。因此,我们最好创建一个`Vector`,利用经典的`Quicksort`(快速排序)方法对其自身进行排序。
编写通用的排序代码时,面临的一个问题是必须根据对象的实际类型来执行比较运算,从而实现正确的排序。当然,一个办法是为每种不同的类型都写一个不同的排序方法。然而,应认识到假若这样做,以后增加新类型时便不易实现代码的重复利用。
程序设计一个主要的目标就是“将发生变化的东西同保持不变的东西分隔开”。在这里,保持不变的代码是通用的排序算法,而每次使用时都要变化的是对象的实际比较方法。因此,我们不可将比较代码“硬编码”到多个不同的排序例程内,而是采用“回调”技术。利用回调,经常发生变化的那部分代码会封装到它自己的类内,而总是保持相同的代码则“回调”发生变化的代码。这样一来,不同的对象就可以表达不同的比较方式,同时向它们传递相同的排序代码。
下面这个“接口”(Interface)展示了如何比较两个对象,它将那些“要发生变化的东西”封装在内:
下面这个“接口”(`Interface`)展示了如何比较两个对象,它将那些“要发生变化的东西”封装在内:
```
//: Compare.java
......@@ -19,9 +19,9 @@ interface Compare {
} ///:~
```
对这两种方法来说,lhs代表本次比较中的“左手”对象,而rhs代表“右手”对象。
对这两种方法来说,`lhs`代表本次比较中的“左手”对象,而`rhs`代表“右手”对象。
可创建Vector的一个子类,通过Compare实现“快速排序”。对于这种算法,包括它的速度以及原理等等,在此不具体说明。欲知详情,可参考Binstock和Rex编著的《Practical Algorithms for Programmers》,由Addison-Wesley于1995年出版。
可创建`Vector`的一个子类,通过`Compare`实现“快速排序”。对于这种算法,包括它的速度以及原理等等,在此不具体说明。欲知详情,可参考Binstock和Rex编著的《Practical Algorithms for Programmers》,由Addison-Wesley于1995年出版。
```
//: SortVector.java
......@@ -66,9 +66,9 @@ public class SortVector extends Vector {
} ///:~
```
现在,大家可以明白“回调”一词的来历,这是由于quickSort()方法“往回调用”了Compare中的方法。从中亦可理解这种技术如何生成通用的、可重复利用(复用)的代码。
现在,大家可以明白“回调”一词的来历,这是由于`quickSort()`方法“往回调用”了`Compare`中的方法。从中亦可理解这种技术如何生成通用的、可重复利用(复用)的代码。
为使用SortVector,必须创建一个类,令其为我们准备排序的对象实现Compare。此时内部类并不显得特别重要,但对于代码的组织却是有益的。下面是针对String对象的一个例子:
为使用`SortVector`,必须创建一个类,令其为我们准备排序的对象实现`Compare`。此时内部类并不显得特别重要,但对于代码的组织却是有益的。下面是针对`String`对象的一个例子:
```
//: StringSortTest.java
......@@ -82,14 +82,14 @@ public class StringSortTest {
return ((String)l).toLowerCase().compareTo(
((String)r).toLowerCase()) < 0;
}
public boolean
public boolean
lessThanOrEqual(Object l, Object r) {
return ((String)l).toLowerCase().compareTo(
((String)r).toLowerCase()) <= 0;
}
}
public static void main(String[] args) {
SortVector sv =
SortVector sv =
new SortVector(new StringCompare());
sv.addElement("d");
sv.addElement("A");
......@@ -107,19 +107,19 @@ public class StringSortTest {
} ///:~
```
内部类是“静态”(Static)的,因为它毋需连接一个外部类即可工作。
内部类是“静态”(`Static`)的,因为它毋需连接一个外部类即可工作。
大家可以看到,一旦设置好框架,就可以非常方便地重复使用象这样的一个设计——只需简单地写一个类,将“需要发生变化”的东西封装进去,然后将一个对象传给SortVector即可。
大家可以看到,一旦设置好框架,就可以非常方便地重复使用象这样的一个设计——只需简单地写一个类,将“需要发生变化”的东西封装进去,然后将一个对象传给`SortVector`即可。
比较时将字符串强制为小写形式,所以大写A会排列于小写a的旁边,而不会移动一个完全不同的地方。然而,该例也显示了这种方法的一个不足,因为上述测试代码按照出现顺序排列同一个字母的大写和小写形式:A a b B c C d D。但这通常不是一个大问题,因为经常处理的都是更长的字符串,所以上述效果不会显露出来(Java 1.2的集合提供了排序功能,已解决了这个问题)。
比较时将字符串强制为小写形式,所以大写`A`会排列于小写`a`的旁边,而不会移动一个完全不同的地方。然而,该例也显示了这种方法的一个不足,因为上述测试代码按照出现顺序排列同一个字母的大写和小写形式:`A a b B c C d D`。但这通常不是一个大问题,因为经常处理的都是更长的字符串,所以上述效果不会显露出来(Java 1.2的集合提供了排序功能,已解决了这个问题)。
继承(extends)在这儿用于创建一种新类型的Vector——也就是说,SortVector属于一种Vector,并带有一些附加的功能。继承在这里可发挥很大的作用,但了带来了问题。它使一些方法具有了final属性(已在第7章讲述),所以不能覆盖它们。如果想创建一个排好序的Vector,令其只接收和生成String对象,就会遇到麻烦。因为addElement()和elementAt()都具有final属性,而且它们都是我们必须覆盖的方法,否则便无法实现只能接收和产生String对象。
继承(`extends`)在这儿用于创建一种新类型的`Vector`——也就是说,`SortVector`属于一种`Vector`,并带有一些附加的功能。继承在这里可发挥很大的作用,但了带来了问题。它使一些方法具有了`final`属性(已在第7章讲述),所以不能覆盖它们。如果想创建一个排好序的`Vector`,令其只接收和生成`String`对象,就会遇到麻烦。因为`addElement()``elementAt()`都具有`final`属性,而且它们都是我们必须覆盖的方法,否则便无法实现只能接收和产生`String`对象。
但在另一方面,请考虑采用“合成”方法:将一个对象置入一个新类的内部。此时,不是改写上述代码来达到这个目的,而是在新类里简单地使用一个SortVector。在这种情况下,用于实现Compare接口的内部类就可以“匿名”地创建。如下所示:
但在另一方面,请考虑采用“合成”方法:将一个对象置入一个新类的内部。此时,不是改写上述代码来达到这个目的,而是在新类里简单地使用一个`SortVector`。在这种情况下,用于实现`Compare`接口的内部类就可以“匿名”地创建。如下所示:
```
//: StrSortVector.java
// Automatically sorted Vector that
// Automatically sorted Vector that
// accepts and produces only Strings
package c08;
import java.util.*;
......@@ -128,15 +128,15 @@ public class StrSortVector {
private SortVector v = new SortVector(
// Anonymous inner class:
new Compare() {
public boolean
public boolean
lessThan(Object l, Object r) {
return
return
((String)l).toLowerCase().compareTo(
((String)r).toLowerCase()) < 0;
}
public boolean
public boolean
lessThanOrEqual(Object l, Object r) {
return
return
((String)l).toLowerCase().compareTo(
((String)r).toLowerCase()) <= 0;
}
......@@ -179,8 +179,8 @@ public class StrSortVector {
} ///:~
```
这样便可快速复用来自SortVector的代码,从而获得希望的功能。然而,并不是来自SortVector和Vector的所有public方法都能在StrSortVector中出现。若按这种形式复用代码,可在新类里为包含类内的每一个方法都生成一个定义。当然,也可以在刚开始时只添加少数几个,以后根据需要再添加更多的。新类的设计最终会稳定下来。
这样便可快速复用来自`SortVector`的代码,从而获得希望的功能。然而,并不是来自`SortVector``Vector`的所有`public`方法都能在`StrSortVector`中出现。若按这种形式复用代码,可在新类里为包含类内的每一个方法都生成一个定义。当然,也可以在刚开始时只添加少数几个,以后根据需要再添加更多的。新类的设计最终会稳定下来。
这种方法的好处在于它仍然只接纳String对象,也只产生String对象。而且相应的检查是在编译期间进行的,而非在运行期。当然,只有addElement()和elementAt()才具备这一特性;elements()仍然会产生一个Enumeration(枚举),它在编译期的类型是未定的。当然,对Enumeration以及在StrSortVector中的类型检查会照旧进行;如果真的有什么错误,运行期间会简单地产生一个异常。事实上,我们在编译或运行期间能保证一切都正确无误吗?(也就是说,“代码测试时也许不能保证”,以及“该程序的用户有可能做一些未经我们测试的事情”)。尽管存在其他选择和争论,使用继承都要容易得多,只是在转换时让人深感不便。同样地,一旦为Java加入参数化类型,就有望解决这个问题。
这种方法的好处在于它仍然只接纳`String`对象,也只产生`String`对象。而且相应的检查是在编译期间进行的,而非在运行期。当然,只有`addElement()``elementAt()`才具备这一特性;`elements()`仍然会产生一个`Enumeration`(枚举),它在编译期的类型是未定的。当然,对`Enumeration`以及在`StrSortVector`中的类型检查会照旧进行;如果真的有什么错误,运行期间会简单地产生一个异常。事实上,我们在编译或运行期间能保证一切都正确无误吗?(也就是说,“代码测试时也许不能保证”,以及“该程序的用户有可能做一些未经我们测试的事情”)。尽管存在其他选择和争论,使用继承都要容易得多,只是在转换时让人深感不便。同样地,一旦为Java加入参数化类型,就有望解决这个问题。
大家在这个类中可以看到有一个名为“sorted”的标志。每次调用addElement()时,都可对Vector进行排序,而且将其连续保持在一个排好序的状态。但在开始读取之前,人们总是向一个Vector添加大量元素。所以与其在每个addElement()后排序,不如一直等到有人想读取Vector,再对其进行排序。后者的效率要高得多。这种除非绝对必要,否则就不采取行动的方法叫作“懒惰求值”(还有一种类似的技术叫作“懒惰初始化”——除非真的需要一个字段值,否则不进行初始化)。
大家在这个类中可以看到有一个名为`sorted`的标志。每次调用`addElement()`时,都可对`Vector`进行排序,而且将其连续保持在一个排好序的状态。但在开始读取之前,人们总是向一个`Vector`添加大量元素。所以与其在每个`addElement()`后排序,不如一直等到有人想读取`Vector`,再对其进行排序。后者的效率要高得多。这种除非绝对必要,否则就不采取行动的方法叫作“懒惰求值”(还有一种类似的技术叫作“懒惰初始化”——除非真的需要一个字段值,否则不进行初始化)。
# 8.6 通用集合库
通过本章的学习,大家已知道标准Java库提供了一些特别有用的集合,但距完整意义的集合尚远。除此之外,象排序这样的算法根本没有提供支持。C++出色的一个地方就是它的库,特别是“标准模板库”(STL)提供了一套相当完整的集合,以及许多象排序和检索这样的算法,可以非常方便地对那些集合进行操作。有感这一现状,并以这个模型为基础,ObjectSpace公司设计了Java版本的“通用集合库”(从前叫作“Java通用库”,即JGL;但JGL这个缩写形式侵犯了Sun公司的版权——尽管本书仍然沿用这个简称)。这个库尽可能遵照STL的设计(照顾到两种语言间的差异)。JGL实现了许多功能,可满足对一个集合库的大多数常规需求,它与C++的模板机制非常相似。JGL包括相互链接起来的列表、设置、队列、映射、栈、序列以及迭代器,它们的功能比Enumeration(枚举)强多了。同时提供了一套完整的算法,如检索和排序等。在某些方面,ObjectSpace的设计也显得比Sun的库设计模式“智能”一些。举个例子来说,JGL集合中的方法不会进入final状态,所以很容易继承和改写那些方法。
通过本章的学习,大家已知道标准Java库提供了一些特别有用的集合,但距完整意义的集合尚远。除此之外,象排序这样的算法根本没有提供支持。C++出色的一个地方就是它的库,特别是“标准模板库”(STL)提供了一套相当完整的集合,以及许多象排序和检索这样的算法,可以非常方便地对那些集合进行操作。有感这一现状,并以这个模型为基础,ObjectSpace公司设计了Java版本的“通用集合库”(从前叫作“Java通用库”,即JGL;但JGL这个缩写形式侵犯了Sun公司的版权——尽管本书仍然沿用这个简称)。这个库尽可能遵照STL的设计(照顾到两种语言间的差异)。JGL实现了许多功能,可满足对一个集合库的大多数常规需求,它与C++的模板机制非常相似。JGL包括相互链接起来的列表、设置、队列、映射、栈、序列以及迭代器,它们的功能比`Enumeration`(枚举)强多了。同时提供了一套完整的算法,如检索和排序等。在某些方面,ObjectSpace的设计也显得比Sun的库设计模式“智能”一些。举个例子来说,JGL集合中的方法不会进入`final状`态,所以很容易继承和改写那些方法。
JGL已包括到一些厂商发行的Java套件中,而且ObjectSpace公司自己也允许所有用户免费使用JGL,包括商业性的使用。详细情况和软件下载可访问 http://www.ObjectSpace.com 。与JGL配套提供的联机文档做得非常好,可作为自己的一个绝佳起点使用。
此差异已折叠。
# 8.8 总结
下面复习一下由标准Java(1.0和1.1)库提供的集合(BitSet未包括在这里,因为它更象一种负有特殊使命的类):
下面复习一下由标准Java(1.0和1.1)库提供的集合(`BitSet`未包括在这里,因为它更象一种负有特殊使命的类):
(1) 数组包含了对象的数字化索引。它容纳的是一种已知类型的对象,所以在查找一个对象时,不必对结果进行转换处理。数组可以是多维的,而且能够容纳基本数据类型。但是,一旦把它创建好以后,大小便不能变化了。
(2) Vector(矢量)也包含了对象的数字索引——可将数组和Vector想象成随机访问集合。当我们加入更多的元素时,Vector能够自动改变自身的大小。但Vector只能容纳对象的引用,所以它不可包含基本数据类型;而且将一个对象引用从集合中取出来的时候,必须对结果进行转换处理。
(2) `Vector`(矢量)也包含了对象的数字索引——可将数组和`Vector`想象成随机访问集合。当我们加入更多的元素时,`Vector`能够自动改变自身的大小。但`Vector`只能容纳对象的引用,所以它不可包含基本数据类型;而且将一个对象引用从集合中取出来的时候,必须对结果进行转换处理。
(3) Hashtable(散列表)属于Dictionary(字典)的一种类型,是一种将对象(而不是数字)同其他对象关联到一起的方式。散列表也支持对对象的随机访问,事实上,它的整个设计模式都在突出访问的“高速度”。
(3) `Hashtable`(散列表)属于`Dictionary`(字典)的一种类型,是一种将对象(而不是数字)同其他对象关联到一起的方式。散列表也支持对对象的随机访问,事实上,它的整个设计模式都在突出访问的“高速度”。
(4) Stack(栈)是一种“后入先出”(LIFO)的队列。
(4) `Stack`(栈)是一种“后入先出”(LIFO)的队列。
若你曾经熟悉数据结构,可能会疑惑为何没看到一套更大的集合。从功能的角度出发,你真的需要一套更大的集合吗?对于Hashtable,可将任何东西置入其中,并以非常快的速度检索;对于Enumeration(枚举),可遍历一个序列,并对其中的每个元素都采取一个特定的操作。那是一种功能足够强劲的工具。
若你曾经熟悉数据结构,可能会疑惑为何没看到一套更大的集合。从功能的角度出发,你真的需要一套更大的集合吗?对于`Hashtable`,可将任何东西置入其中,并以非常快的速度检索;对于`Enumeration`(枚举),可遍历一个序列,并对其中的每个元素都采取一个特定的操作。那是一种功能足够强劲的工具。
Hashtable没有“顺序”的概念。Vector和数组为我们提供了一种线性顺序,但若要把一个元素插入它们任何一个的中部,一般都要付出“惨重”的代价。除此以外,队列、拆散队列、优先级队列以及树都涉及到元素的“排序”——并非仅仅将它们置入,以便以后能按线性顺序查找或移动它们。这些数据结构也非常有用,这也正是标准C++中包含了它们的原因。考虑到这个原因,只应将标准Java库的集合看作自己的一个起点。而且倘若必须使用Java 1.0或1.1,则可在需要超越它们的时候使用JGL。
`Hashtable`没有“顺序”的概念。`Vector`和数组为我们提供了一种线性顺序,但若要把一个元素插入它们任何一个的中部,一般都要付出“惨重”的代价。除此以外,队列、拆散队列、优先级队列以及树都涉及到元素的“排序”——并非仅仅将它们置入,以便以后能按线性顺序查找或移动它们。这些数据结构也非常有用,这也正是标准C++中包含了它们的原因。考虑到这个原因,只应将标准Java库的集合看作自己的一个起点。而且倘若必须使用Java 1.0或1.1,则可在需要超越它们的时候使用JGL。
如果能使用Java 1.2,那么只使用新集合即可,它一般能满足我们的所有需要。注意本书在Java 1.1身上花了大量篇幅,所以书中用到的大量集合都是只能在Java1.1中用到的那些:Vector和Hashtable。就目前来看,这是一个不得以而为之的做法。但是,这样处理亦可提供与老Java代码更出色的向后兼容能力。若要用Java1.2写新代码,新的集合往往能更好地为你服务。
如果能使用Java 1.2,那么只使用新集合即可,它一般能满足我们的所有需要。注意本书在Java 1.1身上花了大量篇幅,所以书中用到的大量集合都是只能在Java1.1中用到的那些:`Vector``Hashtable`。就目前来看,这是一个不得以而为之的做法。但是,这样处理亦可提供与老Java代码更出色的向后兼容能力。若要用Java1.2写新代码,新的集合往往能更好地为你服务。
# 8.9 练习
(1) 新建一个名为Gerbil的类,在构造器中初始化一个int gerbilNumber(类似本章的Mouse例子)。为其写一个名为hop()的方法,用它打印出符合hop()条件的Gerbil的编号。建一个Vector,并为Vector添加一系列Gerbil对象。现在,用elementAt()方法在Vector中遍历,并为每个Gerbil都调用hop()
(1) 新建一个名为`Gerbil`的类,在构造器中初始化一个`int gerbilNumber`(类似本章的`Mouse`例子)。为其写一个名为`hop()`的方法,用它打印出符合`hop()`条件的`Gerbil`的编号。建一个`Vector`,并为`Vector`添加一系列`Gerbil`对象。现在,用`elementAt()`方法在`Vector`中遍历,并为每个`Gerbil`都调用`hop()`
(2) 修改练习1,用Enumeration在调用hop()的同时遍历Vector
(2) 修改练习1,用`Enumeration`在调用`hop()`的同时遍历`Vector`
(3) 在AssocArray.java中,修改这个例子,令其使用一个Hashtable,而不是AssocArray
(3) 在`AssocArray.java`中,修改这个例子,令其使用一个`Hashtable`,而不是`AssocArray`
(4) 获取练习1用到的Gerbil类,改为把它置入一个Hashtable,然后将Gerbil的名称作为一个String(键)与置入表格的每个Gerbil(值)都关联起来。获得用于keys()的一个Enumeration,并用它在Hashtable里遍历,查找每个键的Gerbil,打印出键,然后将gerbil告诉给hop()
(4) 获取练习1用到的`Gerbil`类,改为把它置入一个`Hashtable`,然后将`Gerbil`的名称作为一个`String`(键)与置入表格的每个`Gerbil`(值)都关联起来。获得用于`keys()`的一个`Enumeration`,并用它在`Hashtable`里遍历,查找每个键的`Gerbil`,打印出键,然后将`gerbil`告诉给`hop()`
(5) 修改第7章的练习1,用一个Vector容纳Rodent(啮齿动物),并用Enumeration在Rodent序列中遍历。记住Vector只能容纳对象,所以在访问单独的Rodent时必须采用一个转换(如RTTI)。
(5) 修改第7章的练习1,用一个`Vector`容纳`Rodent`(啮齿动物),并用`Enumeration``Rodent`序列中遍历。记住`Vector`只能容纳对象,所以在访问单独的`Rodent`时必须采用一个转换(如RTTI)。
(6) 转到第7章的中间位置,找到那个GreenhouseControls.java(温室控制)例子,该例应该由三个文件构成。在Controller.java中,类EventSet仅是一个集合。修改它的代码,用一个Stack代替EventSet。当然,这时可能并不仅仅用Stack取代EventSet这样简单;也需要用一个Enumeration遍历事件集。可考虑在某些时候将集合当作Stack对待,另一些时候则当作Vector对待——这样或许能使事情变得更加简单。
(7) (有一定挑战性)在与所有Java发行包配套提供的Java源码库中找出用于Vector的源码。复制这些代码,制作名为
intVector的一个特殊版本,只在其中包含int数据。思考是否能为所有基本数据类型都制作Vector的一个特殊版本。接下来,考虑假如制作一个链接列表类,令其能随同所有基本数据类型使用,那么会发生什么情况。若在Java中提供了参数化类型,利用它们便可自动完成这一工作(还有其他许多好处)。
(6) 转到第7章的中间位置,找到那个`GreenhouseControls.java`(温室控制)例子,该例应该由三个文件构成。在`Controller.java`中,类`EventSet`仅是一个集合。修改它的代码,用一个`Stack`代替`EventSet`。当然,这时可能并不仅仅用`Stack`取代`EventSet`这样简单;也需要用一个`Enumeration`遍历事件集。可考虑在某些时候将集合当作`Stack`对待,另一些时候则当作`Vector`对待——这样或许能使事情变得更加简单。
(7) (有一定挑战性)在与所有Java发行包配套提供的Java源码库中找出用于`Vector`的源码。复制这些代码,制作名为
`intVector`的一个特殊版本,只在其中包含`int`数据。思考是否能为所有基本数据类型都制作`Vector`的一个特殊版本。接下来,考虑假如制作一个链接列表类,令其能随同所有基本数据类型使用,那么会发生什么情况。若在Java中提供了参数化类型,利用它们便可自动完成这一工作(还有其他许多好处)。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册