1. 数组为什么特殊
Java中有大量其他的方式可以持有对象,那么,到底是什么使数组变得与众不同呢?
数组与其他种类的容器之间的区别有三方面:效率、类型和保存基本类型的能力。在Java中数组是一种效率最高的存储和随机访问对象引用序列的方式。数组就是一个简单的线性序列,这使得元素访问非常快速。但是为这种速度所付出的代价是数组对象的大小被固定,并且在其生命周期中不可改变。你可能会建议使用ArrayList,它可以通过创建一个新实例,然后把旧实例中所有的引用移到新实例中,从而实现更多空间的自动分配。尽管通常应该首选ArrayList而不是数组,但是这种弹性需要开销,因此,ArrayList的效率比数组低很多。
数组和容器都可以保证你不能滥用它们。无论你是使用数组还是容器,如果越界,都会得到一个表示程序员错误的RuntimeException异常。
在泛型之前,其他的容器类在处理对象时,都将它们视作没有任何具体类型。也就是所,它们将这些对象都当作Java中所有类的根类Object处理。数组之所以优于泛型之前的容器,就是因为你可以创建一个数组去持有某种具体类型。这意味着你可以通过编译器检查,来防止插入错误类型和抽取不当类型。当然,不论在编译时还是在运行时,Java都会阻止你向对象发送不恰当的消息。所以,并不是说哪种方法更不安全,只是如果编译时就能指出错误,会显得更加优雅,也减少了程序的使用者被异常吓到的可能性。
数组可以持有基本类型,而泛型之前的容器则不能。但是有了泛型,容器就可以指定并检查它们所持有对象的类型,并且有了自动包装机制,容器看起来还能持有基本类型。
数组与泛型容器进行比较的示例:
class BerylliumSphere {
private static long counter;
private final long id = counter++;
public String toString() { return "Sphere " + id; }
}
public class ContainerComparison {
public static void main(String[] args) {
BerylliumSphere[] spheres = new BerylliumSphere[10];
for(int i = 0; i < 5; i++)
spheres[i] = new BerylliumSphere();
print(Arrays.toString(spheres));
print(spheres[4]);
List sphereList =
new ArrayList();
for(int i = 0; i < 5; i++)
sphereList.add(new BerylliumSphere());
print(sphereList);
print(sphereList.get(4));
int[] integers = { 0, 1, 2, 3, 4, 5 };
print(Arrays.toString(integers));
print(integers[4]);
List intList = new ArrayList(
Arrays.asList(0, 1, 2, 3, 4, 5));
intList.add(97);
print(intList);
print(intList.get(4));
}
} /* Output:
[Sphere 0, Sphere 1, Sphere 2, Sphere 3, Sphere 4, null, null, null, null, null]
Sphere 4
[Sphere 5, Sphere 6, Sphere 7, Sphere 8, Sphere 9]
Sphere 9
[0, 1, 2, 3, 4, 5]
4
[0, 1, 2, 3, 4, 5, 97]
4
*/
这两种持有对象的方式都是类型检查型的,并且唯一明显的差异就是数组使用[]来访问元素,而List使用的是add()和get()这样的方法。
随着自动包装机制的出现,容器已经可以与数组几乎一样的用于基本类型中了。数组硕果仅存的优点就是效率。
2. 数组是第一级对象
无论使用哪种类型的数组,数组标识符其实只是一个引用,指向在堆中创建的一个真实对象,这个(数组)对象用以保存指向其他对象的引用。可以作为数组初始化语法的一部分隐式的创建此对象,或者用new表达式显式的创建。只读成员length是数组对象的一部分(事实上,这是唯一一个可以访问的字段或方法),表示此数组对象可以存储多少元素。“[]”语法是访问数组对象唯一的方式。
下例总结了初始化数组的各种方式,以及如何对指向数组的引用赋值,使之指向另一个数组对象。此例也说明,对象数组和基本类型数组在使用上几乎是相同的;唯一的区别就是对象数组保存的数引用,基本类型数组直接保存基本类型的值。
public class ArrayOptions {
public static void main(String[] args) {
// Arrays of objects:
BerylliumSphere[] a; // Local uninitialized variable
BerylliumSphere[] b = new BerylliumSphere[5];
// The references inside the array are
// automatically initialized to null:
print("b: " + Arrays.toString(b));
BerylliumSphere[] c = new BerylliumSphere[4];
for(int i = 0; i < c.length; i++)
if(c[i] == null) // Can test for null reference
c[i] = new BerylliumSphere();
// Aggregate initialization:
BerylliumSphere[] d = { new BerylliumSphere(),new BerylliumSphere(), new BerylliumSphere()
};
// Dynamic aggregate initialization:
a = new BerylliumSphere[]{
new BerylliumSphere(), new BerylliumSphere(),
};
// (Trailing comma is optional in both cases)
print("a.length = " + a.length);
print("b.length = " + b.length);
print("c.length = " + c.length);
print("d.length = " + d.length);
a = d;
print("a.length = " + a.length);
// Arrays of primitives:
int[] e; // Null reference
int[] f = new int[5];
// The primitives inside the array are
// automatically initialized to zero:
print("f: " + Arrays.toString(f));
int[] g = new int[4];
for(int i = 0; i < g.length; i++)
g[i] = i*i;
int[] h = { 11, 47, 93 };
// Compile error: variable e not initialized:
//!print("e.length = " + e.length);
print("f.length = " + f.length);
print("g.length = " + g.length);
print("h.length = " + h.length);
e = h;
print("e.length = " + e.length);
e = new int[]{ 1, 2 };
print("e.length = " + e.length);
}
} /* Output:
b: [null, null, null, null, null]
a.length = 2
b.length = 5
c.length = 4
d.length = 3
a.length = 3
f: [0, 0, 0, 0, 0]
f.length = 5
g.length = 4
h.length = 3
e.length = 3
e.length = 2
*/
数组a是一个尚未初始化的局部变量,在你对它正确的初始化之前,编译器不允许用此引用做任何事情。数组b初始化为指向一个BerylliumSphere引用的数组,但其他并没有BerylliumSphere对象置于数组中。然而,仍然可以询问数组的大小,因为b指向一个合法的对象。这样做有一个小缺点:你无法知道在次数组中确切的有多少元素,因为length只表示数组能容纳多少元素。也就是说,length是数组的大小,而不是实际保存的元素个数。新生成一个数组对象时,其中所有的引用被自动初始化为null;所以检查其中的引用是否为null,即可知道数组的某个位置是否存在对象。同样,基本类型的数组如果是数值型的,就被自动初始化为0;如果是字符型(char)的,就被自动初始化为 ;如果是boolean,就自动初始化为false。
数组c表明,数组对象在创建之后,随即将数组的各个位置都赋值为BerylliumSphere对象。数组d表明使用“聚集初始化”语法创建数组对象(隐式的使用new在堆中创建,就像数组c一样),并且以BerylliumSphere对象将其初始化的过程,这些操作只用了一条语句。下一个数组初始化可以看作是“动态的聚集初始化”。数组d采用的聚集初始化操作必须在定义d的位置使用,但若使用第二种语法,可以在任意位置创建和初始化数组对象。例如,假设方法hide()需要一个BerylliumSphere对象的数组作为输入参数。可以如下调用:hide(d);
但也可以动态的创建将要作为参数传递的数组:hide(new BerylliumSphere[]{new BerylliumSphere});
在许多情况下,此语法使得代码书写变得更方便了。
表达式 a = d;
说明如何将指向某个数组对象的引用赋给另一个数组对象,这与其他类型的对象引用没什么区别。现在a与d都指向堆中的同一个数组对象。
3. 返回一个数组
演示:如何返回String类型数组:
public class IceCream {
private static Random rand = new Random(47);
static final String[] FLAVORS = {
"Chocolate", "Strawberry", "Vanilla Fudge Swirl",
"Mint Chip", "Mocha Almond Fudge", "Rum Raisin",
"Praline Cream", "Mud Pie"
};
public static String[] flavorSet(int n) {
if(n > FLAVORS.length)
throw new IllegalArgumentException("Set too big");
String[] results = new String[n];
boolean[] picked = new boolean[FLAVORS.length];
for(int i = 0; i < n; i++) {
int t;
do
t = rand.nextInt(FLAVORS.length);
while(picked[t]);
results[i] = FLAVORS[t];
picked[t] = true;
}
return results;
}
public static void main(String[] args) {
for(int i = 0; i < 7; i++)
System.out.println(Arrays.toString(flavorSet(3)));
}
} /* Output:
[Rum Raisin, Mint Chip, Mocha Almond Fudge]
[Chocolate, Strawberry, Mocha Almond Fudge]
[Strawberry, Mint Chip, Mocha Almond Fudge]
[Rum Raisin, Vanilla Fudge Swirl, Mud Pie]
[Vanilla Fudge Swirl, Chocolate, Mocha Almond Fudge]
[Praline Cream, Strawberry, Mocha Almond Fudge]
[Mocha Almond Fudge, Strawberry, Mint Chip]
*/
方法flavorSet()创建了一个名为results的String数组。此数组容量为n,由传入方法的参数决定。然后从数组FLAVORS中随机选择元素,存入results数组中,它是方法所最终返回的数组。返回一个数组与返回任何其他对象(实质上是返回引用)没什么区别。
说句题外话,注意当flavorSet()随机选择各种数组元素时,它确保不会重复选择。由一个do循环不断进行随机选择,直到找出一个在数组picked中不存在的元素。(当然,还会比较String以检查随机选择的元素是否已经在数组results中。)如果成功,将此元素加入数组,然后查找下一个(i递增)。
4. 多维数组
创建多维数组很方便。对于基本类型的多维数组,可以通过使用花括号将每个向量分割开:
public class MultidimensionalPrimitiveArray {
public static void main(String[] args) {
int[][] a = {
{ 1, 2, 3, },
{ 4, 5, 6, },
};
System.out.println(Arrays.deepToString(a));
}
} /* Output:
[[1, 2, 3], [4, 5, 6]]
*/
每队花括号括起来的集合都会把你带到下一级数组。下面的示例使用了JavaSE5的Arrays.deepToString()方法,它可以将多维数组转换为多个String,正如从输出中所看到的那样。还可以使用new来分配数组,下面的三维数组就是在new表达式中分配的:
public class ThreeDWithNew {
public static void main(String[] args) {
// 3-D array with fixed length:
int[][][] a = new int[2][2][4];
System.out.println(Arrays.deepToString(a));
}
} /* Output:
[[[0, 0, 0, 0], [0, 0, 0, 0]], [[0, 0, 0, 0], [0, 0, 0, 0]]]
*/
可以看到基本类型数组的值在不进行显式初始化的情况下,会被自动初始化。对象数组会被初始化为null。