本文内容参考自Java8标准
可变参数列表与形式参数有关,需要分两大类来说明,一个是形式参数的数量,一个是形式参数的类型!
一般来讲,形式参数用的最频繁的地方就是类中的方法了。
那么,先来看个方法的例子:
// 有关方法的形式参数举例
//类TestMethod
class TestMethod{
//此处省略类的变量
//方法method,带两个int类型的形式参数
public void Method(int i,int j){
//方法体的内容
}
}
//--------------------------------------------------------------------------
//调用方法method
//类Test
class Test{
//程序的执行入口main方法
public static void main(String[] args){
//创建类TestMethod的对象,引用名称为method
TestMethod method = new TestMethod();
//利用引用名称调用方法Method,并按方法的形式参数传入对应数量和类型的实际参数。
method.Method(5,6);
}
}
上面代码中举例的方法是非常普通的一个方法,这种类型的方法有一个特别明显的局限性:你在创建方法的时候已经完全限定了调用方法的时候能传入的实际参数的类型和个数!比如上面代码举例中的方法Method(int i,int j),调用此方法的时候,你仅仅只能传入两个int类型的实际参数,不能多,也不能少,那么,有没有这么一种可能:
在创建方法的时候,仅限定调用方法时传入的实际参数的类型,但是数量可以随意多少呢?
Java中的可变参数列表就能完全满足上面的需求!
通过利用可变参数列表,你可以在创建方法的时候仅仅规定调用方法的时候传入的实际参数的类型,而数量没有任何限制,可以不传入,可以仅传入一个,可以传入无限多个!
下面通过代码演示如何使用Java中的可变参数列表:
创建一个带有可变参数列表的方法:
// 有关方法的形式参数举例
//类TestMethod
class TestMethod{
//此处省略类的变量
//方法method,使用可变参数列表int...a
//int...a是可变参数列表的形式
//表示类型是int类型的,数量不限定。
public void Method(int...a){
//for循环,循环打印a里面的每一个元素
//这里使用了for循环遍历a里面的所有元素,容易让人觉得a
//实际上就是一个数组,但是不是,只是类似。
for(int i = 0;i<a.length;i++){
//从a[i]的访问形式也容易让人角色a就是数组,但是不是,只是类似.
System.out.println(a[i]);
}
}
}
//--------------------------------------------------------------------------
//调用方法method
//类Test
class Test{
//程序的执行入口main方法
public static void main(String[] args){
//创建类TestMethod的对象,引用名称为method
TestMethod method = new TestMethod();
//不传入任何的实际参数
method.Method();
//传入一个参数(必须是int类型)
method.Method(1);
//传入多个参数(都必须是int类型,相互之间用","隔开)
method.Method(1,5,6,7,8,9,0);
}
}
实际结果示例:
第一次调用,没有传入任何int类型的实际参数,所以输出为" ",就是没有任何内容。
第二次调用,传入了一个1,所以输出内容就是1。
第三次调用,传入了1 ,5 ,6 ,7 ,8 ,9 ,0,所以最后的输出是1567890。
下面再来证实一下为什么可变参数列表不是数组:
还是上面的代码,直接将方法的形式参数替换成数组:
// 有关方法的形式参数举例
//类TestMethod
class TestMethod{
//此处省略类的变量
//方法method,使用数组
public void Method(int[] a){
//for循环,循环打印数组a里面的每一个元素
for(int i = 0;i<a.length;i++){
//a[i]是数组的访问形式
System.out.println(a[i]);
}
}
}
//--------------------------------------------------------------------------
//调用方法method
//类Test
class Test{
//程序的执行入口main方法
public static void main(String[] args){
//创建类TestMethod的对象,引用名称为method
TestMethod method = new TestMethod();
//不传入任何的实际参数
method.Method();
//传入一个参数(必须是int类型)
method.Method(1);
//传入多个参数(都必须是int类型,相互之间用","隔开)
method.Method(1,5,6,7,8,9,0);
}
}
实际结果示例:
从结果中可以看到,将方法的形式参数改成了数组之后,调用方法的时候,Java编译器强制要求也传入数组类型,否则无法通过编译,可见数组与可变参数列表的使用形式完全不同!
再来对比一下可变参数列表和数组:
可变参数列表的形式:
⑴、声明(形式参数):
// 可变参数的声明形式
public void Test(int...a){ }
⑵、使用(实际参数):
// 可变参数的使用形式
//允许不传入任何参数
Test();
//允许只传入一个参数
Test(1);
//允许传入多个参数
Test(1,2,3,4,5.....);
⑶、遍历(与数组的遍历类似)
//可变参数的遍历与数组的遍历类似
//元素的访问也是通过可变参数列表的标识符+中括号[]+元素的下标
//也就是a[i],与数组的完全一致。
for(int i=0;i<a。length;i++){
//元素的访问与数组的完全一致
//切记数组是从0开始计数的,所以它也是一样的
System.out.println(a[i]);
}
数组的形式:
⑴、声明(形式参数):
// 可变参数的声明形式
public void Test(int[] a){ }
⑵、使用(实际参数):
// 可变参数的使用形式
//只允许不传入数组类型
//大括号初始化的数组
Test({1,2,3,4,5});
//new关键字初始化的数组
Test(new int[]{1,2,3,4,5});
//new关键字+默认初始化的数组
//因为目前数组元素都是默认初始化值,后期需要按需覆盖
Test(new int[18]);
⑶、遍历(利用for循环)
//利用for循环遍历数组元素
for(int i=0;i<a。length;i++){
//元素的访问方式:标识符+[]+元素下标
//切记数组是从0开始计数的
System.out.println(a[i]);
}
接下来再讨论一个问题:
形式参数中能不能同时有多种类型的可变参数列表?
用编码工具来测试下:
从编码工具的编译提示来看:一个形式参数列表中只能有一个可变参数列表,并且这个可变参数列表必须是这个形式参数列表的最后一个参数。
从上面的测试中,我们知道了一个形式参数列表中只能存在一个可变参数列表,但是编译器的提示很有意思:可变参数列表只能是最后一个参数!
那么,在一个形式参数列表中,是不是除了可变参数列表外,还能有其他的参数?只是可变参数列表必须放在最后!
用编码工具来测试下:
显然,可变参数列表可以与其他类型的形式参数共存,只是它只能有一个,并且只能放在最后面。
下面的内容可看可不看,实际上明白了可变参数列表的用法就行了。
以下内容节选自Java编程思想第四版:
Java中现在的可变参数列表功能是在Java SE5版本的时候新增的,在Java SE5版本之前想实现"可变参数列表"的话,是如何实现的呢?
是通过数组的形式来实现的(实际上需要解决的问题是如何让一个方法能很好的应用于参数个数以及类型未知的场合,普通方法是没有办法适应参数个数和类型未知的场合的):
由于所有的类都直接或间接继承自Object类型(Java是单根继承结构,这个问题在后期有关继承的博文中会详细描述),所以可以创建以Object数组为参数的方法。
例如:
// Java SE5之前通过数组的功能实现了类似的可变参数列表
//类A
class A{}
//类VarArgs
public class VarArgs{
//静态方法/类方法printArray(Object[] args),形式参数列表为Object类型的数组
static void printArray(Object[] args){
//这不是for循环,这是foreach循环
for(Object obj:args){
//输出obj
System.out.print(obj+" ");
}
//仅仅是换行的作用
System.out.println();
}
//程序的执行入口main方法
public static void main(String[] args){
//调用方法printArray(Object[] args),
//实际参数是一个Object数组,但是数组元素个数可以随意多少个。
//以此达到参数任意多少的目的。
//实际上你还能发现,在这里,Object数组元素分别用了Integer,Float,Double
//三种类型,同时满足了参数类型任意的需求。
//在后面的博文中你会了解到,由于Integer,Float,Double直接或间接继承自
//Object,所以可以将它们看成是Object(这种向上转型在Java中是自动发生的).
printArray(new Object[]{new Integer(47),new Float(3.14),new Double(11.11)});
//与上面的方法调用类似,只不过这里传入的是三个String类型的元素。
//String也是继承自Object.
printArray(new Object[]{"one","two","three"});
//因为你声明的任何自定义类都自动默认继承自Object,
//所以你直接传入A类的对象也是没有问题的。
//但是你需要注意的是,这里传入的是类A的匿名对象。这是合规的。
printArray(new Object[]{new A(),new A(),new A()});
}
}
可以看到print()方法使用Object数组作为参数,然后使用foreach语法遍历整个数组,打印每个对象。
从上面的代码示例中我们可以发现两个关键点:
⑴、利用Java中的继承以及自动向上转型的机制,将方法的形式参数类型设置为Object类型,因为Java中的所有类型都直接或者间接继承自Object类型,所以能满足在实际调用方法的时候传入任何类型都没问题(因为Java中的任何类型实际上都是Object类型)。
⑵、将方法的形式参数设置为数组,这样就不会限制在调用方法的时候传入多少个数组元素,因为数组元素再多也不影响它是一个数组。
通过以上两个方法,就实现了可以将方法用于参数个数以及类型未知的情况(在方法的实际调用中,传入的参数类型任意,个数任意)。
那么,为什么Java SE5还需要特意增加专门的可变参数列表呢?
我们来看下这种使用数组的缺陷:
①、参数不能为空
②、每次传入的参数必须是数组
从上面的示例代码中可以看大,传入的参数必须是数组形式,而不能是普通参数的形式。
③、调用方法的时候必须传入参数,实际参数不能为空
从以上的三点看来,数组的形式只能在某种程度上代替可变参数列表。
你可能看到过像上面这样用数组实现可变参数的Java SE5之前的代码,确实在某种程度上可以产生可变参数列表,在Java SE5中,这种盼望已久的特性终于添加了进来,现在你可以使用它们来定义可变参数列表了:
代码示例:
// Java SE5新增的可变参数列表特性
public class NewVarArgs{
//静态方法/类方法printArray(Object...args),形式参数列表为可变参数列表。
static void printArray(Object[] args){
//这不是for循环,这是foreach循环
for(Object obj:args){
//输出obj
System.out.print(obj+" ");
}
//仅仅是换行的作用
System.out.println();
}
//程序的执行入口main方法
public static void main(String[] args){
//不用数组形式了,直接传入参数。
printArray(new Integer(47),new Float(3.14),new Double(11.11));
//不用数组形式了,直接传入参数。
printArray("one","two","three");
//不用数组形式了,直接传入参数。
printArray(new A(),new A(),new A());
}
}
有了正式的可变参数列表,就不用显示使用数组的语法了。实际上,当你提供了参数以后,编译器会为你去填充数组(这是编译器的内部行为,不再需要用代码去体现了),也就是说你获取的仍然是一个数组,只是形式上不能明显看出来。这也是为什么可以使用foreach遍历的原因,但是,实际上这不仅仅是从元素列表到数组的自动转换。
下面再来看个例子:
上面的示例中传入了一个Integer数组(当然实际上是Oject数组),很明显,编译器肯定会发现它是一个数组,所以不会在它的基础上再做任何的转换,所以,如果你有一组事物,可以将它们当成列表来传递,而如果你已经有了一个数组,该方法可以将它们当做可变参数列表来接受。
继续再来看个例子:
将0个参数传递给可变参数也是可行的。
当参数个数可选的时候,这个特性会非常有用:
// 可变参数列表可选,不传入任何参数也是可以的。
//类OptionalTrailingAguments
public class OptionalTrailingAguments{
//方法f,有一个int类型和一个可变参数列表两种形式参数。
static void f(int required,String...trailing){
//打印required的值。
System.out.println("required=" + required);
//foreach语法遍历可变参数列表trailing
for(String s:trailing){
//输出列表的每一个元素的值。
System.out.println(s+" ");
}
//仅仅是换行
System.out.println();
}
//程序的执行入口main方法
public static void main(String[] args){
//按方法f的形式参数传入了一个1和一个可变参数列表,虽然这个列表只有一个元素"one".
f(1,"one");
//按方法f的形式参数传入了一个1和一个可变参数列表,虽然这个列表只有两个元素"one"和"two".
f(2,"one","two");
//仅仅只有整数0,而没有传入可变参数列表部分.
//这充分说明了可变参数列表参数可选的性质。
f(0);
}
}
以上代码示例还展示了,可变参数列表中的参数类型还可以是除Object之外的类型,这里的可变参数列表类型是String,实际上可变列表参数可以是任何类型,比如基本类型。
下面的例子展示了可变参数列表变为数组的情形:
// 可变参数列表变为数组
//类VarargType
public class VarargType{
//静态方法/类方法f,形式参数为Character类型的可变参数列表
static void f(Character...args){
//输出args所属的类型
System.out.print(args.getClass());
//输出args的长度
System.out.print("length="+args.length);
}
static void g(int...args){
//输出args所属的类型
System.out.print(args.getClass());
//输出args的长度
System.out.print("length="+args.length);
}
//程序的执行入口main方法
public static void main(String[] args){
//调用方法f,传入的实际参数为char类型.
f('a');
//调用方法f,不传入任何的实际参数。
f();
//调用方法g,传入的实际参数为int类型.
g(1);
//调用方法g,不传入任何的实际参数。
g();
//输出"int[]"所属的类型
System.out.print("int[]:"+new int[0].getClass());
}
}
这段示例代码有点复杂,先梳理一下:
方法f的形式参数是Character类型,但是在调用它的时候,传入的实际参数却分别是char类型(‘a’),int类型(0,这里说0是int类型,是因为如果没有任何声明,Java中的默认类型就是int类型),方法g的形式参数是int类型的可变参数列表,调用它的时候传入的实际参数是1(int类型),没有传入任何实际参数。
再来看下这段代码的执行结果:
从结果中能看到,不管最后方法调用的时候传入的实际参数是什么类型,最终生成的数组类型一定是方法的形式参数类型!
getClass()方法将在后面的博文中会提到(有关继承的博文中),它将产生对象的类,并且在直接打印类时,可以直接看到表示该类类型的编码字符串,前导的"["表示这是一个后面紧随类型的数组,而紧随的"I"表示这是一个int类型的数组,为了进行印证,代码的最后一行创建了一个int数组,并打印了其表示的编码字符串。发现可变参数列表实际上也可以是基本类型。并不一定全部都是对象(传入的实际参数是int类型,结果就是int类型的数组,并没有自动地包装成Integer类型)。
下面的例子演示了自动包装机制和可变参数列表的共存:
// 自动包装机制与可变参数列表共存
//类AutoboxingVarargs
public class AutoboxingVarargs{
//方法f,带Integer类型的可变参数列表。
public static void f(Integer...args){
//foreach遍历可变参数列表中的每一个元素
for(Integer i:args){
//打印可变参数列表中的每一个元素。
System.out.print("i"+" ");
}
//仅仅是换行
System.out.println();
}
//程序的执行入口main方法
public static void main(String[] args){
//调用方法f,传入了两个Integer类型的参数。
f(new Integer(1),new Integer(2));
//调用方法f,传入了5个int类型的参数。
//有因为基本类型和对应的对象类型之间有自动转换的机制。
//所以能通过编译,没有任何问题。
f(4,5,6,7,8,9);
//下面这种形式也是顺理成章了。Integer类型和int类型混合。
//编译器能通过编译,因为它们之间会相互转换。
//因为这里的f方法的形式参数是Integer类型的,
//所以这里发生的是int类型自动转换成Integer类型。
//如果方法f的形式参数是int类型,那么这里就是Integer类型转换成int类型。
//千万要记住这个规则。
f(10,new Integer(11),12);
}
}
可变参数列表会使得方法的重载变得复杂:
比如:
// 可变参数列表用于方法重载会变得复杂
//类OverloadintVarargs
public class OverloadintVarargs{
//方法f,带Character类型的可变参数列表。
static void f(Character...args){
//foreach遍历可变参数列表中的每一个元素
for(Character c:args){
//打印可变参数列表中的每一个元素。
System.out.println(c+" ");
}
//仅仅是换行
System.out.println();
}
//重载方法f,带Integer类型的可变参数列表。
static void f(Integer...args){
//foreach遍历可变参数列表中的每一个元素
for(Integer i:args){
//打印可变参数列表中的每一个元素。
System.out.println(i+" ");
}
//仅仅是换行
System.out.println();
}
//重载方法f,带Integer类型的可变参数列表。
static void f(Long...args){
//打印字符串"third"
System.out.println("third");
}
//程序的执行入口main方法
public static void main(String[] args){
//调用方法f,传入了三个char类型的实际参数。
//这里调用的应该是重载方法f(Character...args)
//因为传入的实际参数就是char类型。
f('a','b','c');
//调用方法f,传入了一个int类型的实际参数。
//这里调用的应该是重载方法f(Integer...args)
//因为传入的实际参数就是int类型(Java的默认类型)。
f(1);
//调用方法f,传入了两个int类型的实际参数。
//这里调用的应该是重载方法f(Integer...args)
//因为传入的实际参数就是int类型(Java的默认类型)。
f(2,1);
//调用方法f,传入了一个int类型的实际参数。
//这里调用的应该是重载方法f(Integer...args)
//因为传入的实际参数就是int类型(Java的默认类型)。
f(0);
//调用方法f,传入了一个long类型的实际参数。
//这里调用的应该是重载方法f(Long..args)
//因为传入的实际参数就是long类型。
f(0L);
}
}
在每一种情况中,编译器都会使用自动包装机制来匹配重载的方法,然后调用最明确匹配的方法。
但是你在调用方法f的时候,不添加任何的实际参数,那么编译器就不知道你想要的到底是哪个方法了。
你可能会在某个方法中添加一个非可变参数列表来解决问题,但是:
所以你的解决办法可以是为两个方法都明确加上一个非可变参数列表:
你应该总是在一个重载方法上使用可变参数列表,或者根本就不使用它。
PS:时间有限,有关Java SE的内容会持续更新!今天就先写这么多,如果有疑问或者有兴趣,可以加QQ:2649160693,并注明CSDN,我会就博文中有疑义的问题做出解答。同时希望博文中不正确的地方各位加以指正!