在C语言中初始化数组既容易出错,又相当麻烦。C++ 则通过“聚合初始化(aggregate
initialization)”使其更安全7。在Java中,一切都是对象,也没有类似C++里“聚合”那样的
概念。但它确实提供了数组,也支持数组初始化。
数组只是相同类型的、用一个标识符名称封装到一起的一个对象序列或基本类型数据序列。
数组是通过方括号索引操作符[]来定义和使用的。要定义一个数组,只需在类型名后加上一
对空方括号即可:
int[] a1;
方括号也可以置于标识符后面,其含义相同:
int a1[];
这种格式符合C 和 C++程序员的习惯。不过,前一种格式或许更合理,毕竟它表明类型是
“一个 int型数组”。本书将采用这种格式。
编译器不允许你指定数组的大小。这就又把我们带回到有关“引用”的问题上。现在你拥有
的只是对数组的一个引用,而且也没给数组分配任何空间。为了给数组创建相应的存储空间,
你必须写初始化表达式。对于数组,初始化动作可以出现在代码的任何地方,但也可以使用
一种特殊的初始化表达式,它必须在创建数组的地方出现。这种特殊的初始化是由一对花括
号括起来的值组成的。在这种情况下,存储空间的分配(等价于使用 new)将由编译器负责。
例如:
int[] a1 = { 1, 2, 3, 4, 5 };
那么,为什么还要在没有数组的时候定义一个数组引用呢?
int[] a2;
在 Java 中你可以将一个数组赋值给另一个数组,所以你可以这样:
a2 = a1;
其实你真正做的只是复制了一个引用,就象下面演示的那样:
//:c04:Arrays.java
// Arrays ofprimitives.
import com.bruceeckel.simpletest.*;
public class Arrays {
static Test monitor = new Test();
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++)
System.out.println(
"a1[" + i + "] = " + a1[i]);
monitor.expect(new String[] {
"a1[0] =2",
"a1[1] =3",
"a1[2] =4",
"a1[3] =5",
"a1[4] =6"
});
}
} ///:~
你可以看到代码中给出了 a1 的初始值,但 a2却没有;在本例中,a2 是在后面被赋值为另
一个数组的。
这里有点新东西:所有数组(无论它们的元素是对象还是基本类型)都有一个固有成员,你
可以通过它获知数组内包含了多少个元素,但不能对其修改。这个成员就是 length。与 C和
C++类似,Java 数组计数也是从第 0 个元素开始,所以能使用的最大索引数是“length - 1”。
要是超出这个边界,C 和 C++会“默默”地接受,并允许你访问所有内存,许多声名狼藉的
程序错误由此而生。Java 则能保护你免受这一问题的困扰,一旦访问下标过界,就会出现运
行期错误(即“异常”,将在第 9 章中讨论)。当然,每次访问数组的时候都要检查边界的做
法是需要在时间和代码上有所开销的,但是你无法禁用这个功能。这意味着如果数组访问发
生在一些关键节点上,它们有可能会成为导致程序效率低下的原因之一。但是基于“因特网
的安全以及提高程序员生产力”的理由,Java的设计者认为这是值得的取舍。
如果在编写程序时,你并不能确定在数组里需要多少个元素,那么你该怎么办呢?你可以直
接用 new 在数组里创建元素。尽管创建的是基本类型数组,new 仍然可以工作(不能用 new
创建单个的基本类型数据)。
//:c04:ArrayNew.java
// Creatingarrays with new.
import com.bruceeckel.simpletest.*;
import java.util.*;
public class ArrayNew {
static Test monitor = new Test();
static Random rand = new Random();
public static void main(String[] args) {
int[] a;
a = new int[rand.nextInt(20)];
System.out.println("length of a = " + a.length);
for(int i = 0; i < a.length; i++)
System.out.println("a[" + i + "] = " + a[i]);
monitor.expect(new Object[] {
"%% lengthof a = \\d+",
new TestExpression("%% a\\[\\d+\\] = 0", a.length)
});
}
} ///:~
本例中的expect( )语句里有些新内容:TestExpression 类。要创建 TestExpression对象需要传
入一个表达式,此表达式既可以是普通的字符串,也可以是如例子中的正则表达式;还要传
入一个整形参数用以说明表达式重复的次数。如本例所示,TestExpression 类不仅可以防止
无意义的代码重复,还能在运行时刻决定重复的次数。
数组的大小是通过Random.nextInt( )方法随机决定的,这个方法会返回 0到输入参数之间的
一个值。这表明数组的创建确实是在运行时刻进行的。此外,程序输出表明:数组元素中的
基本数据类型值会自动初始化成“空”值。(对于数字和字符,就是 0;对于布尔型,是 false)。
当然,数组也可以在定义的同时进行初始化:
int[] a = new int[rand.nextInt(20)];
如果可能的话,你应该尽量这么做。
如果数组里的元素不是基本数据类型,那么你必须使用 new。在这里,你会再次遇到引用问
题,因为你创建的数组里每个元素都是一个引用。以整型的包装类 Integer(它可不是基本
类型)为例:
//:c04:ArrayClassObj.java
// Creating anarray of nonprimitive objects.
import com.bruceeckel.simpletest.*;
import java.util.*;
public class ArrayClassObj {
static Test monitor = new Test();
static Random rand = new Random();
public static void main(String[] args) {
Integer[] a = new Integer[rand.nextInt(20)];
System.out.println("length of a = " + a.length);
for(int i = 0; i < a.length; i++) {
a[i] = new Integer(rand.nextInt(500));
System.out.println("a[" + i + "] = " + a[i]);
}
monitor.expect(new Object[] {
"%% lengthof a = \\d+",
new TestExpression("%% a\\[\\d+\\] = \\d+", a.length)
});
}
} ///:~
这里,即便使用new 创建数组之后:
Integer[] a = new Integer[rand.nextInt(20)];
它还只是一个引用数组,并且直到通过创建新的 Integer对象,并且把对象赋值给引用,初
始化进程才算结束:
a[i] = new Integer(rand.nextInt(500));
如果你忘记了创建对象,并且试图使用数组中的空引用,就会在运行时刻产生“异常”。
注意一下打印语句中String 对象的形成,Integer 对象的引用会自动转型,从而产生一个代表
对象内部值的字符
也可以用花括号括起来的列表来初始化对象数组。有两种形式:
//:c04:ArrayInit.java
// Arrayinitialization.
public class ArrayInit {
public static void main(String[] args) {
Integer[] a = {
new Integer(1),
new Integer(2),
new Integer(3),
};
Integer[] b = new Integer[] {
new Integer(1),
new Integer(2),
new Integer(3),
};
}
} ///:~
第一种写法有时很有用,但由于数组的大小在编译期间就决定了,所以很受限制。初始化列
表的最后一个逗号是可选的(这一特性使维护长列表变得更容易)。
第二种形式提供了一种方便的语法来创建对象并调用方法,以获得与 C的“可变参数列表”
(C 通常把它简称为“varargs”)一致的效果。这可以应用于参数个数或类型未知的场合。
由于所有的类都直接或间接继承于 Object 类(随着本书的进展,你会对它有更深入的认识),
所以你可以创建以Object 数组为参数的方法,并这样调用:
//:c04:VarArgs.java
// Using arraysyntax to create variable argument lists.
import com.bruceeckel.simpletest.*;
class A { int i; }
public class VarArgs {
static Test monitor = new Test();
static void print(Object[] x) {
for(int i = 0; i < x.length; i++)
System.out.println(x[i]);
}
public static void main(String[] args) {
print(new Object[] {
new Integer(47), new VarArgs(),
new Float(3.14), new Double(11.11)
});
print(new Object[] {"one", "two", "three" });
print(new Object[] {new A(), new A(), new A()});
monitor.expect(new Object[] {
"47",
"%%VarArgs@\\p{XDigit}+",
"3.14",
"11.11",
"one",
"two",
"three",
new TestExpression("%% A@\\p{XDigit}+", 3)
});
}
} ///:~
你可以看到print( )方法使用 Object 数组作为参数,然后遍历数组,打印每个对象。标准 Java
库中的类能输出有意义的内容,但这里建立的类(A 和 VarArgs)的对象,打印出的内容只
是类的名称以及后面紧跟着的一个‘@’符号。代码里出现的正则表达式\p{XDigit},表示
一个十六进制数字。后面的‘+’表示会有一个或多个十六进制数字。于是,缺省行为(如
果你没有定义 toString( )方法的话,后面会讲这个方法的)就是打印类的名字和对象的地址。