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行为。很难Ԣ