概述
Varargs,即variable number of arguments,variable arguments。中文一般译为,可变长度参数,或简称可变参数,参数具体来说是形参。
自JDK5引入,借助这一机制,可以定义能和多个实参相匹配的形参。
JDK5之前,无法在Java程序里定义实参个数可变的方法,因为Java要求实参(Arguments)和形参(Parameters)的数量和类型都必须逐一匹配,而形参的数目是在定义方法时已确定。尽管可以通过重载机制,为同一个方法提供带有不同数量的形参的版本,但是这仍然不能达到让实参数量任意变化的目的。
定义实参个数可变的方法
只要在一个形参的类型与参数名之间加上…
,则可以让它和不确定个实参相匹配。而一个带有这样的形参的方法,就是一个实参个数可变的方法。只有最后一个形参才能被定义成能和不确定个实参相匹配。故而,一个方法里最多只能有一个这样的形参,而且是在最后的位置。
编译器会在背地里把这最后一个形参转化为一个数组形参,并在编译出的class文件里作上一个记号(什么记号?),表明这是个实参个数可变的方法。
根据J2SE 1.5的语法,在…
前面的空白字符可有可无。故而有两种写法:(Object … args)
和(Object… args)
。Java Code Conventions。究竟哪一种写法比较正统?考虑到数组参数也有Object [] args
和Object[] args
两种书写方式,而推荐的写法是不在[]
前添加空白字符。
调用实参个数可变(包括0个)的方法:sumUp(1, 3, 5, 7);
,编译器会转化为数组包裹实参的形式:sumUp(newint[]{1, 2, 3, 4});
零个实参也可以通过编译:sumUp();
,编译器转化为空数组:sumUp(newint[]{});
处理个数可变的实参的办法,和处理数组实参的办法基本相同。所有的实参,都被保存到一个和形参同名的数组里。
转发个数可变的实参
有时候,在接受一组个数可变的实参之后,还要把它们传递给另一个实参个数可变的方法。因为编码时无法知道接受来的这一组实参的数目,所以“把它们 逐一写到该出现的位置上去”的做法并不可行。不过,这并不意味着这是个不可完成的任务,因为还有另外一种办法,可以用来调用实参个数可变的方法。
在J2SE 1.5编译器中,实参个数可变的方法是最后带了一个数组形参的方法的特例。因此,事先把整组要传递的实参放到一个数组里,然后把这个数组作为最后一个实参,传递给一个实参个数可变的方法,不会造成任何错误。借助这一特性,就可以顺利的完成转发了。
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, Object... args) {
// J2SE 1.5里PrintStream新增的printf(String format, Object... args)方法
System.out.printf(format, args);
}
}
Java里的printf
和sprintf
,C语言里的printf
(按一定的格式输出字符串)和sprintf
(按一定的格式组合字符串)即是Varargs机制的例子。按一定的格式输出字符串的功能,可以通过调用PrintStream.printf(String format, Object… args)
方法来实现。按一定的格式组合字符串的工作,则可以通过调用String.String format(String format, Object… args)
静态方法来进行。
尽管在背地里,编译器会把能匹配不确定个实参的形参,转化为数组形参;而且也可以用数组包实参,再传递给实参个数可变的方法;但是,这并不表示能匹配不确定个实参的形参和数组形参完全没有差异。
一个明显的差异是,如果按照调用实参个数可变的方法的形式,来调用一个最后一个形参是数组形参的方法,只会导致一个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)”的重构手法来近似的达到目的。
当个数可变的实参遇到泛型
泛型,可以在一定条件下把一个类型参数化。泛型机制不能和个数可变的形参配合使用。如果把一个能和不确定个实参相匹配的形参的类型,用一个标识符来代表,那么编译器会给出一个generic array creation
的错误:
private static void testVarargs(T... args) {
// 编译出错
}
原因:J2SE 1.5中的泛型机制有一个内在约束,不能拿用标识符来代表的类型来创建这一类型的实例。
用数组包裹的做法,并不受这个约束的限制:
private static void testVarargs(T[] args) {
for (int i = 0; i < args.length; i++) {
System.out.println(args[i]);
}
}
重载中的选择问题
Java支持重载,允许在同一个类拥有许多只有形参列表不同的方法,然后由编译器根据调用时的实参来选择到底要执行哪一个方法。传统上的选择,基本是依照特殊者优先的原则来进行。一个方法的特殊程度,取决于为了让它顺利运行而需要满足的条件的数目,需要条件越多的越特殊。
在引入Varargs机制之后,这一原则仍然适用,传统上,一个重载方法的各个版本之中,只有形参数量与实参数量正 好一致的那些有被进一步考虑的资格。但是Varargs机制引入之后,完全可以出现两个版本都能匹配,在其它方面也别无二致,只是一个实参个数固定,而一 个实参个数可变的情况。
遇到这种情况时,所用的判定规则是“实参个数固定的版本优先于实参个数可变的版本”。
@Slf4j
public class OverloadingSampleA {
public static void main(String[] args) {
// 打印A
testOverloading(1);
// 打印出B
testOverloading(1, 2);
// 打印出C
testOverloading(1, 2, 3);
}
private static void testOverloading(int i) {
log.info("A");
}
private static void testOverloading(int i, int j) {
log.info("B");
}
private static void testOverloading(int i, int... more) {
log.info("C");
}
}
在编译器看来,同时有多个方法具有相同的优先权,它就会陷入无法就到底调用哪个方法作出一个选择的状态。在这样的时候,它就会产生一个reference to 被调用的方法名 is ambiguous
的编译错误。
在引入Varargs机制之后,这种可能导致迷惑的情况,又增加了一些。例如现在可能会有两个版本都能匹配,在其它方面也如出一辙,而且都是实参个数可变的冲突发生。
public class OverloadingSampleB {
public static void main(String[] args) {
// 编译出错: ambiguous method call, both a and b match
testOverloading(1, 2, 3);
}
private static void testOverloading(Object... args) {
}
private static void testOverloading(Object o, Object... args) {
}
}
由于自动装箱和自动拆箱机制的存在,所以还可能发生两个版本都能匹配,而且都是实参个数可变,其它方面也一模一样,只是一个能接受的实参是基本类型,而另一个能接受的实参是包裹类的冲突发生。
public class OverloadingSampleC {
public static void main(String[] args) {
// 编译出错
testOverloading(1, 2);
//还是编译出错
testOverloading(new Integer(1), new Integer(2));
}
private static void testOverloading(int... args) {
}
private static void testOverloading(Integer... args) {
}
}
归纳总结
和用数组包裹相比,实参个数可变的方法,在调用时传递参数的操作更为简单,含义也更为清楚。不过也有局限。
用数组包裹实参
直接看代码:
private int sum(int[] nums){
return 0;
}
Varargs机制
Varargs机制,允许直接定义能和多个实参相匹配的形参。从而,可以用一种更简单的方式,来传递个数可变的实参。
工作原理
可变参数在被使用的时候,他首先会创建一个数组,数组的长度就是调用该方法是传递的实参的个数,然后再把参数值全部放到这个数组当中,然后再把这个数组作为参数传递到被调用的方法中。
语法糖
语法糖是一种几乎每种语言或多或少都提供过的一些方便程序员开发代码的语法,编译期间以特定的字节码或者特定的方式对这些语法做一些处理,开发者就可以直接方便地使用。这些语法糖虽然不会提供实质性的功能改进,但是它们或能提高性能、或能提升语法的严谨性、或能减少编码出错的机会。Java提供给用户大量的语法糖,比如泛型、自动装箱、自动拆箱、foreach循环、变长参数、内部类、枚举类、断言(assert)等。
可变长度参数
先讲可变长度参数:
public static void main(String[] args) {
print("000", "111", "222", "333");
}
public static void print(String... strs) {
for (int i = 0; i < strs.length; i++) {
System.out.println(strs[i]);
}
}
print方法的参数的意思是表示传入的String个数是不定的,运行结果:
000
111
222
333
我用数组遍历的方式成功地将输入的参数遍历出来了,这说明两个问题:
1、可以使用遍历数组的方式去遍历可变参数
2、可变参数是利用数组实现的
其实main函数也可以这么写,完全可以:
String[] strs = {"000", "111", "222", "333"};
print(strs);
可变长度参数必须作为方法参数列表中的的最后一个参数且方法参数列表中只能有一个可变长度参数。
foreach循环原理
自己写一个ArrayList,想用foreach循环遍历一下看一下写的效果,结果报空指针异常。代码:
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
list.add("111");
list.add("222");
for (String str : list) {
System.out.println(str);
}
}
javap反编译一下:javap -verbose TestMain.class
截取一段信息:
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: new #16 // class java/util/ArrayList
3: dup
4: invokespecial #18 // Method java/util/ArrayList."<in
it>":()V
7: astore_1
8: aload_1
9: ldc #19 // String 111
11: invokeinterface #21, 2 // InterfaceMethod java/util/List.
add:(Ljava/lang/Object;)Z
16: pop
17: aload_1
18: ldc #27 // String 222
20: invokeinterface #21, 2 // InterfaceMethod java/util/List.
add:(Ljava/lang/Object;)Z
25: pop
26: aload_1
27: invokeinterface #29, 1 // InterfaceMethod java/util/List.
iterator:()Ljava/util/Iterator;
new、dup、invokespecial这些本来就是字节码指令表内定义的指令,虚拟机会根据这些指令去执行指定的C++代码,完成每个指令的功能。关键21、22行的iterator,得出结论:在编译的时候编译器会自动将对for这个关键字的使用转化为对目标的迭代器的使用,这就是foreach循环的原理。得出两个结论:
1、ArrayList之所以能使用foreach循环遍历,是因为ArrayList所有的List都是Collection的子接口,而Collection是Iterable的子接口,ArrayList的父类AbstractList正确地实现了Iterable接口的iterator方法。之前我自己写的ArrayList用foreach循环直接报空指针异常是因为我自己写的ArrayList并没有实现Iterable接口
2、任何一个集合,无论是JDK提供的还是自己写的,只要想使用foreach循环遍历,就必须正确地实现Iterable接口。
实际上,这种做法就是23中设计模式中的迭代器模式。
数组并没有实现Iterable接口,为什么可以用foreach?
public static void main(String[] args) {
int[] ints = {1,2,3,4,5};
for (int i : ints) {
System.out.println(i);
}
}
反编译:
0: iconst_2
1: newarray int
3: dup
4: iconst_0
5: iconst_1
6: iastore
7: dup
8: iconst_1
9: iconst_2
10: iastore
11: astore_1
12: aload_1
13: dup
14: astore 5
16: arraylength
17: istore 4
19: iconst_0
20: istore_3
21: goto 39
24: aload 5
26: iload_3
27: iaload
28: istore_2
29: getstatic #16 // Field java/lang/System.out:Ljava/io/PrintStream;
32: iload_2
33: invokevirtual #22 // Method java/io/PrintStream.println:(I)V
36: iinc 3, 1
39: iload_3
40: iload 4
42: if_icmplt 24
45: return
完整的这段main函数对应的45个字节码指令,涉及一些压栈、出栈、推送等。简单对照字节码指令表之后,我个人对于这45个字节码的理解是Java将对于数组的foreach循环转换为对于这个数组每一个的循环引用。
【强制】相同参数类型,相同业务含义,才可以使用 Java 的可变参数,避免使用 Object。
说明: 可变参数必须放置在参数列表的最后。(提倡同学们尽量不用可变参数编程)
正例:public User getUsers(String type, Integer... ids)
白话:
用处不大,可以用重载方法或者数组参数代替。
一般应用在日志的 API 定义上,用于传不定的日志参数。
参考
可变长度参数以及foreach循环原理