java使⽤省略号代替多参数(参数类型...参数名)
J2SE 1.5提供了“Varargs”机制。借助这⼀机制,可以定义能和多个实参相匹配的形参。从⽽,可以⽤⼀种更简单的⽅式,来传递个数可变的实参。本⽂介绍这⼀机制的使⽤⽅法,以及这⼀机制与数组、泛型、重载之间的相互作⽤时的若⼲问题。
到J2SE 1.4为⽌,⼀直⽆法在Java程序⾥定义实参个数可变的⽅法——因为Java要求实参(Arguments)和形参(Parameters)的数量和类型都必须逐⼀匹配,⽽形参的数⽬是在定义⽅法时就已经固定下来了。尽管可以通过重载机制,为同⼀个⽅法提供带有不同数量的形参的版本,但是这仍然不能达到让实参数量任意变化的⽬的。
然⽽,有些⽅法的语义要求它们必须能接受个数可变的实参——例如著名的main⽅法,就需要能接受所有的命令⾏参数为实参,⽽命令⾏参数的数⽬,事先根本⽆法确定下来。
对于这个问题,传统上⼀般是采⽤“利⽤⼀个数组来包裹要传递的实参”的做法来应付。
1. ⽤数组包裹实参
“⽤数组包裹实参”的做法可以分成三步:⾸先,为这个⽅法定义⼀个数组型的参数;然后在调⽤时,⽣成⼀个包含了所有要传递的实参的数组;最后,把这个数组作为⼀个实参传递过去。
这种做法可以有效的达到“让⽅法可以接受个数可变的参数”的⽬的,只是调⽤时的形式不够简单。
J2SE 1.5中提供了Varargs机制,允许直接定义能和多个实参相匹配的形参。从⽽,可以⽤⼀种更简单的⽅式,来传递个数可变的实参。Varargs的含义
⼤体说来,“Varargs”是“variable number of arguments”的意思。有时候也被简单的称为“variable arguments”,不过因为这⼀种叫法没有说明是什么东西可变,所以意义稍微有点模糊。
2. 定义实参个数可变的⽅法
只要在⼀个形参的“类型”与“参数名”之间加上三个连续的“.”(即“...”,英⽂⾥的句中省略号),就可以让它和不确定个实参相匹配。⽽⼀个带有这样的形参的⽅法,就是⼀个实参个数可变的⽅法。
清单1:⼀个实参个数可变的⽅法
private static int sumUp( values) {
}
注意,只有最后⼀个形参才能被定义成“能和不确定个实参相匹配”的。因此,⼀个⽅法⾥只能有⼀个这样的形参。另外,如果这个⽅法还有其它的形参,要把它们放到前⾯的位置上。
编译器会在背地⾥把这最后⼀个形参转化为⼀个数组形参,并在编译出的class⽂件⾥作上⼀个记号,表明这是个实参个数可变的⽅法。
清单2:实参个数可变的⽅法的秘密形态
private static int sumUp( int[] values) {
}
由于存在着这样的转化,所以不能再为这个类定义⼀个和转化后的⽅法签名⼀致的⽅法。
清单3:会导致编译错误的组合
private static int sumUp( values) {
}
private static int sumUp( int[] values) {
}
空⽩的存亡问题
根据J2SE 1.5的语法,在“...”前⾯的空⽩字符是可有可⽆的。这样就有在“...”前⾯添加空⽩字符(形如“Object ... args”)和在“...”前⾯不加空⽩字符(形如“ args”)的两种写法。因为⽬前和J2SE 1.5相配合的Java Code Conventions还没有正式发布,所以⽆法知道究竟哪⼀种写法⽐较正统。不过,考虑到数组参数也有“Object [] args”和“Object[] args”两种书写⽅式,⽽正统的写法是不在“[]”前添加空⽩字符,似乎采取不加空⽩的“ args”的写法在整体上更协调⼀些。
3. 调⽤实参个数可变的⽅法
只要把要传递的实参逐⼀写到相应的位置上,就可以调⽤⼀个实参个数可变的⽅法。不需要其它的步骤。
清单4:可以传递若⼲个实参
sumUp( 1, 3, 5, 7);
在背地⾥,编译器会把这种调⽤过程转化为⽤“数组包裹实参”的形式:
清单5:偷偷出现的数组创建
sumUp( new int[]{1, 2, 3, 4});
另外,这⾥说的“不确定个”也包括零个,所以这样的调⽤也是合乎情理的:
清单6:也可以传递零个实参
sumUp ();
这种调⽤⽅法被编译器秘密转化之后的效果,则等同于这样:
清单7:零实参对应空数组
sumUp(new int[] {});
注意这时传递过去的是⼀个空数组,⽽不是null。这样就可以采取统⼀的形式来处理,⽽不必检测到底属于哪种情况。
4. 处理个数可变的实参
处理个数可变的实参的办法,和处理数组实参的办法基本相同。所有的实参,都被保存到⼀个和形参同名的数组⾥。根据实际的需要,把这个数组⾥的元素读出之后,要蒸要煮,就可以随意了。
清单8:处理收到的实参们
private static int values) {
int sum = 0;
for (int i = 0; i < values.length; i++) {
sum += values[i];
}
return sum;
}
5. 转发个数可变的实参
有时候,在接受了⼀组个数可变的实参之后,还要把它们传递给另⼀个实参个数可变的⽅法。因为编码时⽆法知道接受来的这⼀组实参的数⽬,所以“把它们逐⼀写到该出现的位置上去”的做法并不可⾏。不过,这并不意味着这是个不可完成的任务,因为还有另外⼀种办法,可以⽤来调⽤实参个数可变的⽅法。
在J2SE 1.5的编译器的眼中,实参个数可变的⽅法是最后带了⼀个数组形参的⽅法的特例。因此,事先把整组要传递的实参放到⼀个数组⾥,然后把这个数组作为最后⼀个实参,传递给⼀个实参个数可变的⽅法,不会造成任何错误。借助这⼀特性,就可以顺利的完成转发了。
清单9:转发收到的实参们
public class PrintfSample {
public static void main(String[] args) {
//打印出“Pi:3.141593 E:2.718282”
printOut("Pi:%f E:%f/n", Math.PI, Math.E);
}
private static void printOut(String format, args) {
//J2SE 1.5⾥PrintStream新增的printf(String format, args)⽅法
System.out.printf(format, args);
}
}
Java⾥的“printf”和“sprintf”
C语⾔⾥的printf(按⼀定的格式输出字符串)和sprintf(按⼀定的格式组合字符串)是⼗分经典的使⽤Varargs机制的例⼦。在J2SE 1.5中,也分别在java.io.PrintStream类和java.lang.String类中提供了类似的功能。
按⼀定的格式输出字符串的功能,可以通过调⽤PrintStream对象的printf(String format, args)⽅法来实现。
按⼀定的格式组合字符串的⼯作,则可以通过调⽤String类的String format(String format, args)静态⽅法来进⾏。
6. 是数组?不是数组?
尽管在背地⾥,编译器会把能匹配不确定个实参的形参,转化为数组形参;⽽且也可以⽤数组包了实参,再传递给实参个数可变的⽅法;但是,这并不表⽰“能匹配不确定个实参的形参”和“数组形参”完全没有差异。
⼀个明显的差异是,如果按照调⽤实参个数可变的⽅法的形式,来调⽤⼀个最后⼀个形参是数组形参的⽅法,只会导致⼀个“cannot be applied to”的编译错误。
清单10:⼀个“cannot be applied to”的编译错误
private static void testOverloading( int[] i) {
System.out.println("A");
}
public static void main(String[] args) {
testOverloading( 1, 2, 3);//编译出错
}
由于这⼀原因,不能在调⽤只⽀持⽤数组包裹实参的⽅法的时候(例如在不是专门为J2SE 1.5设计第三⽅类库中遗留的那些),直接采⽤这种简明的调⽤⽅式。
如果不能修改原来的类,为要调⽤的⽅法增加参数个数可变的版本,⽽⼜想采⽤这种简明的调⽤⽅式,
那么可以借助“引⼊外加函数(Introduce Foreign Method)”和“引⼊本地扩展(Intoduce Local Extension)”的重构⼿法来近似的达到⽬的。
7. 当个数可变的实参遇到泛型
J2SE 1.5中新增了“泛型”的机制,可以在⼀定条件下把⼀个类型参数化。例如,可以在编写⼀个类的时候,把⼀个⽅法的形参的类型⽤⼀个标识符(如T)来代表,⾄于这个标识符到底表⽰什么类型,则在⽣成这个类的实例的时候再⾏指定。这⼀机制可以⽤来提供更充分的代码重⽤和更严格的编译时类型检查。
不过泛型机制却不能和个数可变的形参配合使⽤。如果把⼀个能和不确定个实参相匹配的形参的类型,⽤⼀个标识符来代表,那么编译器会给出⼀个“generic array creation”的错误。
清单11:当Varargs遇上泛型
private static <T> void testVarargs( T... args) {//编译出错
}
造成这个现象的原因在于J2SE 1.5中的泛型机制的⼀个内在约束——不能拿⽤标识符来代表的类型来创
建这⼀类型的实例。在出现⽀持没有了这个约束的Java版本之前,对于这个问题,基本没有太好的解决办法。
不过,传统的“⽤数组包裹”的做法,并不受这个约束的限制。
清单12:可以编译的变通做法
private static <T> void testVarargs( T[] args) {
for (int i = 0; i < args.length; i++) {
System.out.println(args[i]);
}
}
8. 重载中的选择问题
Java⽀持“重载”的机制,允许在同⼀个类拥有许多只有形参列表不同的⽅法。然后,由编译器根据调⽤时的实参来选择到底要执⾏哪⼀个⽅法。
传统上的选择,基本是依照“特殊者优先”的原则来进⾏。⼀个⽅法的特殊程度,取决于为了让它顺利运⾏⽽需要满⾜的条件的数⽬,需要条件越多的越特殊。
在引⼊Varargs机制之后,这⼀原则仍然适⽤,只是要考虑的问题丰富了⼀些——传统上,⼀个重载⽅法的各个版本之中,只有形参数量与实参数量正好⼀致的那些有被进⼀步考虑的资格。但是Varargs机制引⼊之后,完全可以出现两个版本都能匹配,在其它⽅⾯也别⽆⼆致,只是⼀个实参个数固定,⽽⼀个实参个数可变的情况。
遇到这种情况时,所⽤的判定规则是“实参个数固定的版本优先于实参个数可变的版本”。
清单13:实参个数固定的版本优先
public class OverloadingSampleA {
public static void main(String[] args) {
testOverloading( 1);//打印出A
testOverloading( 1, 2);//打印出B
testOverloading( 1, 2, 3);//打印出C
}
private static void testOverloading( int i) {
System.out.println("A");
}
private static void testOverloading( int i, int j) {
System.out.println("B");
}
private static void testOverloading( int i, more) {
System.out.println("C");
}
}
如果在编译器看来,同时有多个⽅法具有相同的优先权,它就会陷⼊⽆法就到底调⽤哪个⽅法作出⼀个选择的状态。在这样的时候,它就会产⽣⼀个“reference to 被调⽤的⽅法名 is ambiguous”的编译错误,并耐⼼的等候作了⼀些修改,⾜以免除它的迷惑的新源代码的到来。
在引⼊了Varargs机制之后,这种可能导致迷惑的情况,⼜增加了⼀些。例如现在可能会有两个版本都能匹配,在其它⽅⾯也如出⼀辙,⽽且都是实参个数可变的冲突发⽣。
清单14:左右都不是,为难了编译器
public class OverloadingSampleB {
public static void main(String[] args) {
testOverloading(1, 2, 3);//编译出错
}
private static void testOverloading( args) {
}
private static void testOverloading( Object o, args) {
}
}
另外,因为J2SE 1.5中有“Autoboxing/Auto-Unboxing”机制的存在,所以还可能发⽣两个版本都能匹配,⽽且都是实参个数可变,其它⽅⾯也⼀模⼀样,只是⼀个能接受的实参是基本类型,⽽另⼀个能接受的实参是包裹类的冲突发⽣。
printf输出格式java清单15:Autoboxing/Auto-Unboxing带来的新问题
public class OverloadingSampleC {
public static void main(String[] args) {
/* 编译出错 */
testOverloading( 1, 2);
/* 还是编译出错 */
testOverloading( new Integer(1), new Integer(2));
}
private static void testOverloading( args) {
}
private static void testOverloading( args) {
}
}
9. 归纳总结
和“⽤数组包裹”的做法相⽐,真正的实参个数可变的⽅法,在调⽤时传递参数的操作更为简单,含义也更为清楚。不过,这⼀机制也有它⾃⾝的局限,并不是⼀个完美⽆缺的解决⽅案。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论