4.5 数组初始化

在C中初始化数组极易出错,而且相当麻烦。C++通过“集合初始化”使其更安全(注释⑥)。Java则没有象C++那样的“集合”概念,因为Java中的所有东西都是对象。但它确实有自己的数组,通过数组初始化来提供支持。

数组代表一系列对象或者基本数据类型,所有相同的类型都封装到一起——采用一个统一的标识符名称。数组的定义和使用是通过方括号索引运算符进行的([])。为定义一个数组,只需在类型名后简单地跟随一对空方括号即可:

int[] al;

也可以将方括号置于标识符后面,获得完全一致的结果:

int al[];

这种格式与C和C++程序员习惯的格式是一致的。然而,最“通顺”的也许还是前一种语法,因为它指出类型是“一个int数组”。本书将沿用那种格式。

编译器不允许我们告诉它一个数组有多大。这样便使我们回到了“句柄”的问题上。此时,我们拥有的一切就是指向数组的一个句柄,而且尚未给数组分配任何空间。为了给数组创建相应的存储空间,必须编写一个初始化表达式。对于数组,初始化工作可在代码的任何地方出现,但也可以使用一种特殊的初始化表达式,它必须在数组创建的地方出现。这种特殊的初始化是一系列由花括号封闭起来的值。存储空间的分配(等价于使用new)将由编译器在这种情况下进行。例如:

int[] a1 = { 1, 2, 3, 4, 5 };

那么为什么还要定义一个没有数组的数组句柄呢?

int[] a2;

事实上在Java中,可将一个数组分配给另一个,所以能使用下述语句:

a2 = a1;

我们真正准备做的是复制一个句柄,就象下面演示的那样:

//: Arrays.java
// Arrays of primitives.
public class Arrays {
  public static void main(String[] args) {
    int[] a1 = { 1, 2, 3, 4, 5 };
    int[] a2;
    a2 = a1;
    for(int i = 0; i < a2.length; i++)
      a2[i]++;
    for(int i = 0; i < a1.length; i++)
      prt("a1[" + i + "] = " + a1[i]);
  }
  static void prt(String s) {
    System.out.println(s);
  }
} ///:~

大家看到a1获得了一个初始值,而a2没有;a2将在以后赋值——这种情况下是赋给另一个数组。

这里也出现了一些新东西:所有数组都有一个本质成员(无论它们是对象数组还是基本类型数组),可对其进行查询——但不是改变,从而获知数组内包含了多少个元素。这个成员就是length。与C和C类似,由于Java数组从元素0开始计数,所以能索引的最大元素编号是“length-1”。如超出边界,C和C会“默默”地接受,并允许我们胡乱使用自己的内存,这正是许多程序错误的根源。然而,Java可保留我们这受这一问题的损害,方法是一旦超过边界,就生成一个运行期错误(即一个“违例”,这是第9章的主题)。当然,由于需要检查每个数组的访问,所以会消耗一定的时间和多余的代码量,而且没有办法把它关闭。这意味着数组访问可能成为程序效率低下的重要原因——如果它们在关键的场合进行。但考虑到因特网访问的安全,以及程序员的编程效率,Java设计人员还是应该把它看作是值得的。

程序编写期间,如果不知道在自己的数组里需要多少元素,那么又该怎么办呢?此时,只需简单地用new在数组里创建元素。在这里,即使准备创建的是一个基本数据类型的数组,new也能正常地工作(new不会创建非数组的基本类型):

//: ArrayNew.java
// Creating arrays with new.
import java.util.*;
public class ArrayNew {
  static Random rand = new Random();
  static int pRand(int mod) {
    return Math.abs(rand.nextInt()) % mod + 1;
  }
  public static void main(String[] args) {
    int[] a;
    a = new int[pRand(20)];
    prt("length of a = " + a.length);
    for(int i = 0; i < a.length; i++)
      prt("a[" + i + "] = " + a[i]);
  }
  static void prt(String s) {
    System.out.println(s);
  }
} ///:~

由于数组的大小是随机决定的(使用早先定义的pRand()方法),所以非常明显,数组的创建实际是在运行期间进行的。除此以外,从这个程序的输出中,大家可看到基本数据类型的数组元素会自动初始化成“空”值(对于数值,空值就是零;对于char,它是null;而对于boolean,它却是false)。

当然,数组可能已在相同的语句中定义和初始化了,如下所示:

int[] a = new int[pRand(20)];

若操作的是一个非基本类型对象的数组,那么无论如何都要使用new。在这里,我们会再一次遇到句柄问题,因为我们创建的是一个句柄数组。请大家观察封装器类型Integer,它是一个类,而非基本数据类型:

//: ArrayClassObj.java
// Creating an array of non-primitive objects.
import java.util.*;
public class ArrayClassObj {
  static Random rand = new Random();
  static int pRand(int mod) {
    return Math.abs(rand.nextInt()) % mod + 1;
  }
  public static void main(String[] args) {
    Integer[] a = new Integer[pRand(20)];
    prt("length of a = " + a.length);
    for(int i = 0; i < a.length; i++) {
      a[i] = new Integer(pRand(500));
      prt("a[" + i + "] = " + a[i]);
    }
  }
  static void prt(String s) {
    System.out.println(s);
  }
} ///:~

在这儿,甚至在new调用后才开始创建数组:

Integer[] a = new Integer[pRand(20)];

它只是一个句柄数组,而且除非通过创建一个新的Integer对象,从而初始化了对象句柄,否则初始化进程不会结束:

a[i] = new Integer(pRand(500));

但若忘记创建对象,就会在运行期试图读取空数组位置时获得一个“违例”错误。

下面让我们看看打印语句中String对象的构成情况。大家可看到指向Integer对象的句柄会自动转换,从而产生一个String,它代表着位于对象内部的值。

亦可用花括号封闭列表来初始化对象数组。可采用两种形式,第一种是Java 1.0允许的唯一形式。第二种(等价)形式自Java 1.1才开始提供支持:

//: ArrayInit.java
// Array initialization
public class ArrayInit {
  public static void main(String[] args) {
    Integer[] a = {
      new Integer(1),
      new Integer(2),
      new Integer(3),
    };
    // Java 1.1 only:
    Integer[] b = new Integer[] {
      new Integer(1),
      new Integer(2),
      new Integer(3),
    };
  }
} ///:~

这种做法大多数时候都很有用,但限制也是最大的,因为数组的大小是在编译期间决定的。初始化列表的最后一个逗号是可选的(这一特性使长列表的维护变得更加容易)。

数组初始化的第二种形式(Java 1.1开始支持)提供了一种更简便的语法,可创建和调用方法,获得与C的“变量参数列表”(C通常把它简称为“变参表”)一致的效果。这些效果包括未知的参数(自变量)数量以及未知的类型(如果这样选择的话)。由于所有类最终都是从通用的根类Object中继承的,所以能创建一个方法,令其获取一个Object数组,并象下面这样调用它:

//: VarArgs.java
// Using the Java 1.1 array syntax to create
// variable argument lists
class A { int i; }
public class VarArgs {
  static void f(Object[] x) {
    for(int i = 0; i < x.length; i++)
      System.out.println(x[i]);
  }
  public static void main(String[] args) {
    f(new Object[] { 
        new Integer(47), new VarArgs(), 
        new Float(3.14), new Double(11.11) });
    f(new Object[] {"one", "two", "three" });
    f(new Object[] {new A(), new A(), new A()});
  }
} ///:~

此时,我们对这些未知的对象并不能采取太多的操作,而且这个程序利用自动String转换对每个Object做一些有用的事情。在第11章(运行期类型标识或RTTI),大家还会学习如何调查这类对象的准确类型,使自己能对它们做一些有趣的事情。

4.5.1 多维数组

在Java里可以方便地创建多维数组:

//: MultiDimArray.java
// Creating multidimensional arrays.
import java.util.*;
public class MultiDimArray {
  static Random rand = new Random();
  static int pRand(int mod) {
    return Math.abs(rand.nextInt()) % mod + 1;
  }
  public static void main(String[] args) {
    int[][] a1 = {
      { 1, 2, 3, },
      { 4, 5, 6, },
    };
    for(int i = 0; i < a1.length; i++)
      for(int j = 0; j < a1[i].length; j++)
        prt("a1[" + i + "][" + j +
            "] = " + a1[i][j]);
    // 3-D array with fixed length:
    int[][][] a2 = new int[2][2][4];
    for(int i = 0; i < a2.length; i++)
      for(int j = 0; j < a2[i].length; j++)
        for(int k = 0; k < a2[i][j].length;
            k++)
          prt("a2[" + i + "][" +
              j + "][" + k +
              "] = " + a2[i][j][k]);
    // 3-D array with varied-length vectors:
    int[][][] a3 = new int[pRand(7)][][];
    for(int i = 0; i < a3.length; i++) {
      a3[i] = new int[pRand(5)][];
      for(int j = 0; j < a3[i].length; j++)
        a3[i][j] = new int[pRand(5)];
    }
    for(int i = 0; i < a3.length; i++)
      for(int j = 0; j < a3[i].length; j++)
        for(int k = 0; k < a3[i][j].length;
            k++)
          prt("a3[" + i + "][" +
              j + "][" + k +
              "] = " + a3[i][j][k]);
    // Array of non-primitive objects:
    Integer[][] a4 = {
      { new Integer(1), new Integer(2)},
      { new Integer(3), new Integer(4)},
      { new Integer(5), new Integer(6)},
    };
    for(int i = 0; i < a4.length; i++)
      for(int j = 0; j < a4[i].length; j++)
        prt("a4[" + i + "][" + j +
            "] = " + a4[i][j]);
    Integer[][] a5;
    a5 = new Integer[3][];
    for(int i = 0; i < a5.length; i++) {
      a5[i] = new Integer[3];
      for(int j = 0; j < a5[i].length; j++)
        a5[i][j] = new Integer(i*j);
    }
    for(int i = 0; i < a5.length; i++)
      for(int j = 0; j < a5[i].length; j++)
        prt("a5[" + i + "][" + j +
            "] = " + a5[i][j]);
  }
  static void prt(String s) {
    System.out.println(s);
  }
} ///:~

用于打印的代码里使用了length,所以它不必依赖固定的数组大小。 第一个例子展示了基本数据类型的一个多维数组。我们可用花括号定出数组内每个矢量的边界:

int[][] a1 = {
{ 1, 2, 3, },
{ 4, 5, 6, },
};

每个方括号对都将我们移至数组的下一级。 第二个例子展示了用new分配的一个三维数组。在这里,整个数组都是立即分配的: int[][][] a2 = new int[2][2][4]; 但第三个例子却向大家揭示出构成矩阵的每个矢量都可以有任意的长度:

    int[][][] a3 = new int[pRand(7)][][];
    for(int i = 0; i < a3.length; i++) {
      a3[i] = new int[pRand(5)][];
      for(int j = 0; j < a3[i].length; j++)
        a3[i][j] = new int[pRand(5)];
    }

对于第一个new创建的数组,它的第一个元素的长度是随机的,其他元素的长度则没有定义。for循环内的第二个new则会填写元素,但保持第三个索引的未定状态——直到碰到第三个new。 根据输出结果,大家可以看到:假若没有明确指定初始化值,数组值就会自动初始化成零。 可用类似的表式处理非基本类型对象的数组。这从第四个例子可以看出,它向我们演示了用花括号收集多个new表达式的能力:

    Integer[][] a4 = {
      { new Integer(1), new Integer(2)},
      { new Integer(3), new Integer(4)},
      { new Integer(5), new Integer(6)},
    };

第五个例子展示了如何逐渐构建非基本类型的对象数组:

    Integer[][] a5;
    a5 = new Integer[3][];
    for(int i = 0; i < a5.length; i++) {
      a5[i] = new Integer[3];
      for(int j = 0; j < a5[i].length; j++)
        a5[i][j] = new Integer(i*j);
    }

i*j只是在Integer里置了一个有趣的值。

下一节:作为初始化的一种具体操作形式,构建器应使大家明确感受到在语言中进行初始化的重要性。与C++的程序设计一样,判断一个程序效率如何,关键是看是否由于变量的初始化不正确而造成了严重的编程错误(臭虫)。这些形式的错误很难发现,而且类似的问题也适用于不正确的清除或收尾工作。由于构建器使我们能保证正确的初始化和清除(若没有正确的构建器调用,编译器不允许对象创建),所以能获得完全的控制权和安全性。

在C++中,与“构建”相反的“破坏”(Destruction)工作也是相当重要的,因为用new创建的对象必须明确地清除。在Java中,垃圾收集器会自动为所有对象释放内存,所以Java中等价的清除方法并不是经常都需要用到的。如果不需要类似于构建器的行为,Java的垃圾收集器可以极大简化编程工作,而且在内存的管理过程中增加更大的安全性。有些垃圾收集器甚至能清除其他资源,比如图形和文件句柄等。然而,垃圾收集器确实也增加了运行期的开销。但这种开销到底造成了多大的影响却是很难看出的,因为到目前为止,Java解释器的总体运行速度仍然是比较慢的。随着这一情况的改观,我们应该能判断出垃圾收集器的开销是否使Java不适合做一些特定的工作(其中一个问题是垃圾收集器不可预测的性质)。

由于所有对象都肯定能获得正确的构建,所以同这儿讲述的情况相比,构建器实际做的事情还要多得多。特别地,当我们通过“创作”或“继承”生成新类的时候,对构建的保证仍然有效,而且需要一些附加的语法来提供对它的支持。大家将在以后的章节里详细了解创作、继承以及它们对构建器造成的影响。