初始化与清理(5):数组初始化、可变参数列表

一、数组初始化

    数组只是相同类型的、用一个标识符名称封装到一起的对象序列或基本类型数据序列。数组是通过方括号下标操作符[]来定义和使用的。要定义一个数组,只需在类型名后加上一对空方括号即可:

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');
	}
}

    你应该总是只在重载方法的一个版本上使用可变参数列表,或者压根就不用它。

发布了71 篇原创文章 · 获赞 2 · 访问量 6080

猜你喜欢

转载自blog.csdn.net/qq_40298351/article/details/104075125