一、数组初始化
数组只是相同类型的、用一个标识符名称封装到一起的对象序列或基本类型数据序列。数组是通过方括号下标操作符[]来定义和使用的。要定义一个数组,只需在类型名后加上一对空方括号即可:
int[] a1;
方括号也可以置于标识符后面:
int a1[];
两种格式含义是一样的,前一种格式或许更合理,毕竟它表明类型是“一个int型数组”。
编译器不允许指定数组的大小。这就又把我们带回到有关“引用”的问题上。现在拥有的只是对数组的一个引用(你已经为该引用分配了足够的存储空间),而且也没给数组对象本身分配任何空间。为了给数组创建相应的存储空间,必须写初始化表达式。对于数组,初始化动作可以出现在代码的任何地方,但也可以使用一种特殊的初始化表达式,它必须在创建数组的地方出现。这种特殊的初始化是由一对花括号括起来的值组成的。在这种情况下,存储空间的分配(等价于使用new)将由编译器负责。例如:
int [] a1 = {1, 2, 3, 4, 5};
那么,为什么还要在没有数组的时候定义一个数组引用呢?int[] a2;在java中可以将一个数组赋值给另一个数组,所以可以这样:a2 = a1;其实真正做的只是复制了一个引用,就像下面演示的那样:
public class ArraysOfPrimitives {
public static void main(String[] args) {
int[] a1 = { 1, 2, 3, 4, 5 };
int[] a2 = a1;
for (int i = 0; i < a2.length; i++)
a2[i] = a2[i] + 1;
for (int i = 0; i < a1.length; i++)
System.out.println("a1[" + i + "]=" + a1[i]);
}
}
可以看到代码中给出了a1的初始值,但a2却没有;在本例中,a2是在后面被赋给另一个数组的。由于a2和a1是相同数组的别名,因此通过a2所做的修改在a1中可以看到。
所有数组(无论它们的元素是对象还是基本类型)都有一个固有成员,可以通过它获知数组内包含了多少个元素,但不能对其修改。这个成员就是length。与C和C++类似,java数组计数也是从第0个元素开始,所以能使用的最大下标数是length-1。要是超出这个边界,C和C++会“默默”地接受,并允许你访问所有内存,许多声明狼藉的程序错误由此而生。java则能保护你免受这一问题的困扰,一旦访问下标过界,就会出现运行时错误(即异常)。
如果在编写程序时,并不能确定数组里需要多少个元素,那么该怎么办呢?可以直接用new在数组里创建元素。尽管创建的是基本类型数组,new仍然可以工作(不能用new创建单个的基本类型数据)。
import java.util.Arrays;
import java.util.Random;
public class ArrayNew {
public static void main(String[] args) {
int[] a;
Random r = new Random();
a = new int[r.nextInt(20)];
System.out.println("length of a=" + a.length);
System.out.println(Arrays.toString(a));
}
}
数组的大小是通过r.nextInt()方法随机决定的,这个方法会返回0到输出参数之间的一个值。这表明数组的创建确实是在运行时刻进行的。此外,程序输出表明:数组元素中的基本数据类型值会自动初始化成空值(对于数字和字符,就是0;对于布尔型,是false)。
Arrrays.toString()方法属于java.util标准类库,它将产生一维数组的可打印版本。
当然,在本例中,数组也可以在定义的同时进行初始化:
int[] a = new int[r.nextInt(20)];
如果可能的话,应该尽量这么做。
如果你创建了一个非基本类型的数组,那么你就创建了一个引用数组。以整型的包装器类Integer为例,它是一个类而不是基本类型:
import java.util.Arrays;
import java.util.Random;
public class ArrayClassObj {
public static void main(String[] args) {
Random r = new Random();
Integer[] a = new Integer[r.nextInt(20)];
System.out.println("length of a=" + a.length);
for (int i = 0; i < a.length; i++)
a[i] = r.nextInt(500);
System.out.println(Arrays.deepToString(a));
}
}
这里,即便使用new创建数组之后:
Integer[] a = new Integer[r.nextInt(20)];
它还只是一个引用数组,并且直到通过创建新的Integer对象(在本例中是通过自动包装机制创建的),并把对象赋值给引用,初始化进程才算结束:
a[i] = r.nextInt(500);
如果忘记了创建对象,并且试图使用数组中的空引用,就会在运行时产生异常。
也可以用花括号括起来的列表来初始化对象数组。有两种形式:
import java.util.Arrays;
public class ArrayInit {
public static void main(String[] args) {
Integer[] a = { new Integer(1), new Integer(2), 3 };
Integer[] b = new Integer[] { new Integer(1), new Integer(2), 3 };
System.out.println(Arrays.toString(a));
System.out.println(Arrays.toString(b));
}
}
尽管第一种形式很有用,但是它也更加受限,因为它只能用于数组被定义处。你可以在任何地方使用第二种和第三种形式,甚至是在方法调用的内部。例如,你可以创建一个String对象数组,将其传递给另一个main()方法,以提供参数,用来替换传递给该main()方法的命令行参数。
public class DynamicArray {
public static void main(String[] args) {
Other.main(new String[] { "fiddle", "de", "dum" });
}
}
class Other {
public static void main(String[] args) {
for (String string : args)
System.out.println(string + " ");
}
}
为Other.main()的参数而创建的数组是在方法调用处创建的,因此你甚至可以在调用时提供可替换的参数。
二、可变参数列表
第二种形式提供了一种方便的语法来创建对象并调用方法,以获得与C的可变参数列表一样的效果。这可以应用于参数个数或类型未知的场合。由于所有的类都直接或间接继承于Object类,所以可以创建以Object数组为参数的方法,并像下面这样调用:
class A {
}
public class VarArgs {
static void printArray(Object[] args) {
for (Object object : args)
System.out.print(object + " ");
System.out.println();
}
public static void main(String[] args) {
printArray(new Object[] { new Integer(47), new Float(3.14), new Double(11.11) });
printArray(new Object[] { "one", "two", "three" });
printArray(new Object[] { new A(), new A(), new A() });
}
}
可以看到print()方法使用Object数组作为参数,然后使用foreach语法遍历数组,打印每个对象。标准java库中的类能输出有意义的内容,但这里建立的类的对象,打印出的内容只是类的名称以及后面紧跟着一个@符号以及多个十六进制数字。于是,默认行为就是打印类的名称和对象的地址。
在java SE5以后,你可以使用可变参数列表。例如:
public class NewVarArgs {
static void printArray(Object... args) {
for (Object object : args)
System.out.print(object + " ");
System.out.println();
}
public static void main(String[] args) {
printArray(new Object[] { new Integer(47), new Float(3.14), new Double(11.11) });
printArray(47, 3.14F, 11.11);
printArray("one", "two", "three");
printArray(new A(), new A(), new A());
printArray((Object[]) new Integer[] { 1, 2, 3, 4 });
printArray();
}
}
有了可变参数,就再也不用显示地编写数组语法了,当你指定参数时,编译器实际上会为你去填充数组。你获取的仍旧是一个数组,这就是为什么print()可以使用foreach来迭代该数组的原因。但是,这不仅仅只是从元素列表到数组的自动转换,请注意程序中倒数第二行,一个Integer数组(通过使用自动包装而创建的)被转型为一个Object数组(以便移除编译器警告信息),并且传递给了printArray()。很明显,编译器会发现他已经是一个数组了,所以不会在其上执行任何转换。因此,如果你有一组事物,可以把它们当作列表传递,而如果你已经有了一个数组,该方法可以把它们当作可变参数列表来接受。
该程序的最后一行表明将0个参数传递给可变参数列表是可行的,当具有可选的尾随参数时,这一特性就会很有用:
public class OptionalTrailingArguments {
static void f(int required, String... trailing) {
System.out.print("required: " + required + " ");
for (String string : trailing)
System.out.print(string + " ");
System.out.println();
}
public static void main(String[] args) {
f(1, "one");
f(2, "two", "three");
f(0);
}
}
这个程序还展示了你可以如何使用具有Object之外类型的可变参数列表。这里所有的可变参数都必须是String对象。在可变参数列表中可以使用任何类型的对参数,包括基本类型。下面的例子也展示了可变参数列表变为数组的情形,并且如果在该列表中没有任何元素,那么转变成的数据尺寸为0:
public class VarargType {
static void f(Character... args) {
System.out.println(args.getClass());
System.out.println(" length " + args.length);
}
static void g(int... args) {
System.out.println(args.getClass());
System.out.println(" length " + args.length);
}
public static void main(String[] args) {
f('a');
f();
g(1);
g();
System.out.println("int[]: " + new int[0].getClass());
}
}
getClass()方法属于Object的一部分,它将产生对象的类,并且打印该类时,可以看到表示该类类型的编码字符串。前导的“[”表示这是一个后面紧随的类型的数组,而紧随的“I”表示基本类型int。为了进行双重检查,我在最后一行创建了一个int数组,并打印了其类型。这样也就验证了使用可变参数列表不依赖于自动包装机制,而实际上使用的是基本类型。
然而,可变参数列表与自动包装机制可以和谐共处,例如:
public class AutoboxingVarargs {
public static void f(Integer... args) {
for (Integer integer : args)
System.out.print(integer + " ");
System.out.println();
}
public static void main(String[] args) {
f(new Integer(1), new Integer(2));
f(4, 5, 6, 7, 8, 9);
f(10, new Integer(11), 12);
}
}
请注意,你可以在单一的参数列表中将类型混合在一起,而自动包装机制将有选择地将int参数提升为Integer。
可变参数列表使得重载过程变得复杂了,尽管乍一看会显得足够安全:
public class OverloadingVarargs {
static void f(Character... args) {
System.out.print("first");
for (Character character : args)
System.out.print(" " + character);
System.out.println();
}
static void f(Integer... args) {
System.out.print("second");
for (Integer integer : args)
System.out.print(" " + integer);
System.out.println();
}
static void f(Long... args) {
System.out.println("third");
}
public static void main(String[] args) {
f('a', 'b', 'c');
f(1);
f(2, 1);
f(0);
f(0L);
// f();
}
}
在每一种情况中,编译器都会使用自动包装机制来匹配重载的方法,然后调用最明确的匹配的方法。
但是在不使用参数调用f()时,编译器就无法知道应该调用哪一个方法了,尽管这个错误可以弄清楚,但是它可能会使客户端程序员大感意外。
你可能会通过在某个方法中增加一个非可变参数来解决该问题:
public class OverloadingVarargs2 {
static void f(float i, Character... args) {
System.out.println("first");
}
static void f(Character... args) {
System.out.println("second");
}
public static void main(String[] args) {
f(1, 'a');
//f('a', 'b');
}
}
如果你给这两个方法都添加一个非可变参数,就可以解决问题了:
public class OverloadingVarargs2 {
static void f(float i, Character... args) {
System.out.println("first");
}
static void f(char c, Character... args) {
System.out.println("second");
}
public static void main(String[] args) {
f(1, 'a');
f('a', 'b');
}
}
你应该总是只在重载方法的一个版本上使用可变参数列表,或者压根就不用它。