ITEEDU

8.7.8 实用工具

Collections类中含有其他大量有用的实用工具:

enumeration(Collection) Produces an old-style Enumeration for the argument.
max(Collection) min(Collection) Produces the maximum or minimum element in the argument using the natural comparison method of the objects in the Collection.
max(Collection, Comparator) min(Collection, Comparator) Produces the maximum or minimum element in the Collection using the Comparator.
nCopies(int n, Object o) Returns an immutable List of size n whose handles all point to o.
subList(List, int min, int max) Returns a new List backed by the specified argument List that is a window into that argument with indexes starting at min and stopping just before max.

enumeration(Collection) 为自变量产生原始风格的Enumeration(枚举)

max(Collection),min(Collection)

在自变量中用集合内对象的自然比较方法产生最大或最小元素

max(Collection,Comparator),min(Collection,Comparator)

在集合内用比较器产生最大或最小元素

nCopies(int n, Object o) 返回长度为n的一个不可变列表,它的所有句柄均指向o

subList(List,int min,int max)

返回由指定参数列表后推得到的一个新列表。可将这个列表想象成一个“窗口”,它自索引为min的地方开始,正好结束于max的前面

注意min()和max()都是随同Collection对象工作的,而非随同List,所以不必担心Collection是否需要排序(就象早先指出的那样,在执行一次binarySearch()——即二进制搜索——之前,必须对一个List或者一个数组执行sort())。

1. 使Collection或Map不可修改

通常,创建Collection或Map的一个“只读”版本显得更有利一些。Collections类允许我们达到这个目标,方法是将原始容器传递进入一个方法,并令其传回一个只读版本。这个方法共有四种变化形式,分别用于Collection(如果不想把集合当作一种更特殊的类型对待)、List、Set以及Map。下面这个例子演示了为它们分别构建只读版本的正确方法:

//: ReadOnly.java
// Using the Collections.unmodifiable methods
package c08.newcollections;
import java.util.*;

public class ReadOnly {
  public static void main(String[] args) {
    Collection c = new ArrayList();
    Collection1.fill(c); // Insert useful data
    c = Collections.unmodifiableCollection(c);
    Collection1.print(c); // Reading is OK
    //! c.add("one"); // Can't change it
    
    List a = new ArrayList();
    Collection1.fill(a);
    a = Collections.unmodifiableList(a);
    ListIterator lit = a.listIterator();
    System.out.println(lit.next()); // Reading OK
    //! lit.add("one"); // Can't change it

    Set s = new HashSet();
    Collection1.fill(s);
    s = Collections.unmodifiableSet(s);
    Collection1.print(s); // Reading OK
    //! s.add("one"); // Can't change it
    
    Map m = new HashMap();
    Map1.fill(m, Map1.testData1);
    m = Collections.unmodifiableMap(m);
    Map1.print(m); // Reading OK
    //! m.put("Ralph", "Howdy!");
  }
} ///:~

对于每种情况,在将其正式变为只读以前,都必须用有有效的数据填充容器。一旦载入成功,最佳的做法就是用“不可修改”调用产生的句柄替换现有的句柄。这样做可有效避免将其变成不可修改后不慎改变其中的内容。在另一方面,该工具也允许我们在一个类中将能够修改的容器保持为private状态,并可从一个方法调用中返回指向那个容器的一个只读句柄。这样一来,虽然我们可在类里修改它,但其他任何人都只能读。

为特定类型调用“不可修改”的方法不会造成编译期间的检查,但一旦发生任何变化,对修改特定容器的方法的调用便会产生一个UnsupportedOperationException违例。

2. Collection或Map的同步

synchronized关键字是“多线程”机制一个非常重要的部分。我们到第14章才会对这一机制作深入的探讨。在这儿,大家只需注意到Collections类提供了对整个容器进行自动同步的一种途径。它的语法与“不可修改”的方法是类似的:

//: Synchronization.java
// Using the Collections.synchronized methods
package c08.newcollections;
import java.util.*;

public class Synchronization {
  public static void main(String[] args) {
    Collection c = 
      Collections.synchronizedCollection(
        new ArrayList());
    List list = Collections.synchronizedList(
      new ArrayList());
    Set s = Collections.synchronizedSet(
      new HashSet());
    Map m = Collections.synchronizedMap(
      new HashMap());
  }
} ///:~

在这种情况下,我们通过适当的“同步”方法直接传递新容器;这样做可避免不慎暴露出未同步的版本。

新集合也提供了能防止多个进程同时修改一个容器内容的机制。若在一个容器里反复,同时另一些进程介入,并在那个容器中插入、删除或修改一个对象,便会面临发生冲突的危险。我们可能已传递了那个对象,可能它位位于我们前面,可能容器的大小在我们调用size()后已发生了收缩——我们面临各种各样可能的危险。针对这个问题,新的集合库集成了一套解决的机制,能查出除我们的进程自己需要负责的之外的、对容器的其他任何修改。若探测到有其他方面也准备修改容器,便会立即产生一个ConcurrentModificationException(并发修改违例)。我们将这一机制称为“立即失败”——它并不用更复杂的算法在“以后”侦测问题,而是“立即”产生违例。

8.8 总结

下面复习一下由标准Java(1.0和1.1)库提供的集合(BitSet未包括在这里,因为它更象一种负有特殊使命的类):

(1) 数组包含了对象的数字化索引。它容纳的是一种已知类型的对象,所以在查找一个对象时,不必对结果进行造型处理。数组可以是多维的,而且能够容纳基本数据类型。但是,一旦把它创建好以后,大小便不能变化了。

(2) Vector(矢量)也包含了对象的数字索引——可将数组和Vector想象成随机访问集合。当我们加入更多的元素时,Vector能够自动改变自身的大小。但Vector只能容纳对象的句柄,所以它不可包含基本数据类型;而且将一个对象句柄从集合中取出来的时候,必须对结果进行造型处理。

(3) Hashtable(散列表)属于Dictionary(字典)的一种类型,是一种将对象(而不是数字)同其他对象关联到一起的方式。散列表也支持对对象的随机访问,事实上,它的整个设计方案都在突出访问的“高速度”。

(4) Stack(堆栈)是一种“后入先出”(LIFO)的队列。

若你曾经熟悉数据结构,可能会疑惑为何没看到一套更大的集合。从功能的角度出发,你真的需要一套更大的集合吗?对于Hashtable,可将任何东西置入其中,并以非常快的速度检索;对于Enumeration(枚举),可遍历一个序列,并对其中的每个元素都采取一个特定的操作。那是一种功能足够强劲的工具。

但Hashtable没有“顺序”的概念。Vector和数组为我们提供了一种线性顺序,但若要把一个元素插入它们任何一个的中部,一般都要付出“惨重”的代价。除此以外,队列、拆散队列、优先级队列以及树都涉及到元素的“排序”——并非仅仅将它们置入,以便以后能按线性顺序查找或移动它们。这些数据结构也非常有用,这也正是标准C++中包含了它们的原因。考虑到这个原因,只应将标准Java库的集合看作自己的一个起点。而且倘若必须使用Java 1.0或1.1,则可在需要超越它们的时候使用JGL。如果能使用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()。

(2) 修改练习1,用Enumeration在调用hop()的同时遍历Vector。

(3) 在AssocArray.java中,修改这个例子,令其使用一个Hashtable,而不是AssocArray。

(4) 获取练习1用到的Gerbil类,改为把它置入一个Hashtable,然后将Gerbil的名称作为一个String(键)与置入表格的每个Gerbil(值)都关联起来。获得用于keys()的一个Enumeration,并用它在Hashtable里遍历,查找每个键的Gerbil,打印出键,然后将gerbil告诉给hop()。

(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中提供了参数化类型,利用它们便可自动完成这一工作(还有其他许多好处)。