java 变参数_Java 中的变长参数

在 Java 5 中提供了变长参数,允许在调用方法时传入不定长度的参数。变长参数是Java的一个语法糖,本质上还是基于数组的实现。1

2void foo(String... args);

void foo(String[] args);1

2//方法签名

([Ljava/lang/String;)V // public void foo(String[] args)

从方法的签名可以看到,变长参数在编译为字节码后,在方法签名中就是以数组形态出现的。这两个方法的签名是一致的,不能作为方法的重载。如果同时出现,是不能编译通过的。可变参数可以兼容数组,反之则不成立。1

2

3

4

5

6public void foo(String...varargs){}

foo("arg1", "arg2", "arg3");

//上述过程和下面的调用是等价的

foo(new String[]{"arg1", "arg2", "arg3"});

使用规则

优先匹配固定参数1

2

3

4

5

6

7

8

9

10void foo(String arg1, String arg2) {

System.out.println("invoke first method!");

}

void foo(String... args) {

System.out.println("invoke second method!");

}

foo("arg1", "arg2"); //invoke first method!

foo("arg1", "arg2", "arg3"); //invoke second method!

如果同时匹配多个可变参数,无法编译通过1

2

3

4

5

6

7

8

9void foo(String arg1, String... args) {

System.out.println("invoke first method!");

}

void foo(String... args) {

System.out.println("invoke second method!");

}

foo("arg1");//compile error

可变参数只能有一个,且必须在参数列表最后

规范

在使用变长参数时,有必要遵循一些规范,使得别人更容易理解你的代码。以下三个建议来自“编写高质量代码:改善Java进程的151个建议”一书。

避免带有变长参数的方法重载

即便编译器可以按照优先匹配固定参数的方式确定具体的调用方法,但在阅读代码的依然容易掉入陷阱。要慎重考虑变长参数的方法重载。

别让null值和空值威胁到变长方法1

2

3

4

5

6

7

8

9

10

11

12

13

14

15public class Client {

public void methodA(String str,Integer... is){

}

public void methodA(String str,String... strs){

}

public static void main(String[] args) {

Client client = new Client();

client.methodA("China", 0);

client.methodA("China", "People");

client.methodA("China"); //compile error

client.methodA("China",null); //compile error

}

}

修改如下:1

2

3

4

5public static void main(String[] args) {

Client client = new Client();

String[] strs = null;

client.methodA("China",strs);

}

让编译器知道这个null值是String类型的,编译即可顺利通过,也就减少了错误的发生。

覆写变长方法也要循规蹈矩

在子类中覆写父类的方法,需要满足以下几个条件:

覆写方法不能缩小访问权限。

参数列表必须与被覆写方法相同。

返回类型必须与被覆写方法的相同或是其子类。

覆写方法不能抛出新的异常,或者超出父类范围的异常,但是可以抛出更少、更有限的异常,或者不抛出异常。

我们看一下下面的这个例子:1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24public class Client {

public static void main(String[] args) {

//向上转型

Base base = new Sub();

base.fun(100, 50);

//不转型

Sub sub = new Sub();

sub.fun(100, 50); //compile error

}

}

//基类

class Base{

void fun(int price, int... discounts){

System.out.println("Base......fun");

}

}

//子类,覆写父类方法

class Sub extends Base{

@Override

void fun(int price, int[] discounts){

System.out.println("Sub......fun");

}

}

子类中覆写父类的方法是没有问题的,因为方法的签名是一致的,父类的 fun 方法中 int… 参数在编译成字节码后是 int[], 子类在参数列表中直接使用 int[] 是一致的。然而在编译时 main 方法中第二个调用却会出错。

这太奇怪了:子类继承了父类的所有属性和方法,甭管是私有的还是公开的访问权限,同样的参数、同样的方法名,通过父类调用没有任何问题,通过子类调用却编译通不过,为啥?难道是没继承下来?或者子类缩小了父类方法的前置条件?那如果是这样,就不应该覆写,@Override就应该报错,真是奇妙的事情!

事实上,base对象是把子类对象Sub做了向上转型,形参列表是由父类决定的,由于是变长参数,在编译时,“base.fun(100, 50)”中的“50”这个实参会被编译器“猜测”而编译成“{50}”数组,再由子类Sub执行。我们再来看看直接调用子类的情况,这时编译器并不会把“50”做类型转换,因为数组本身也是一个对象,编译器还没有聪明到要在两个没有继承关系的类之间做转换,要知道Java是要求严格的类型匹配的,类型不匹配编译器自然就会拒绝执行,并给予错误提示。

这是个特例,覆写的方法参数列表与父类不完全相同,这违背了覆写的定义,并且会引发莫名其妙的错误。所以在对变长参数进行覆写时,如果要使用此类似的方法,请仔细想想是不是一定要如此。在进行方法覆写的时候,方法参数要与父类完全一致,不仅仅是类型、数量,还包括显示形式。

可能会踩的坑

使用 Object… 作为变长参数1

2

3

4

5

6

7

8

9

10

11public void foo(Object... args) {

System.out.println(args.length);

}

foo(new String[]{"arg1", "arg2", "arg3"}); //3

foo(100, new String[]{"arg1", "arg1"}); //2

foo(new Integer[]{1, 2, 3}); //3

foo(100, new Integer[]{1, 2, 3}); //2

foo(1, 2, 3); //3

foo(new int[]{1, 2, 3}); //1

int[] 无法转型为 Object[], 因而被当作一个单纯的数组对象 ; Integer[] 可以转型为 Object[], 可以作为一个对象数组。

反射方法调用时的注意事项1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18public class Test {

public static void foo(String... varargs){

System.out.println(args.length);

}

public static void main(String[] args){

String[] varArgs = new String[]{"arg1", "arg2"};

try{

Method method = Test.class.getMethod("foo", String[].class);

method.invoke(null, varArgs);

method.invoke(null, (Object[])varArgs);

method.invoke(null, (Object)varArgs);

method.invoke(null, new Object[]{varArgs});

} catch (Exception e){

e.printStackTrace();

}

}

}

上面的四个调用中,前两个都会在运行时抛出java.lang.IllegalArgumentException: wrong number of arguments异常,后两个则正常调用。

反射是运行时获取的,在运行时看来,可变长参数和数组是一致的,因而方法签名为:1

2//方法签名

([Ljava/lang/String;)V // public void foo(String[] varargs)

再来看一下 Method 对象的方法声明:1Object invoke(Object obj, Object... args)

args 虽然是一个可变长度的参数,但是 args 的长度是受限于该方法对象代表的真实方法的参数列表长度的,而从运行时签名来看,([Ljava/lang/String;)V实际上只有一个形参,即String[] varargs,因而invoke(Object obj, Object... args)中可变参数 args 的实参长度只能为1。1

2

3

4

5

6//Object invoke(Object obj, Object... args)

//String[] varArgs = new String[]{"arg1", "arg2"};

method.invoke(null, varArgs); //varArgs长度为2,错误

method.invoke(null, (Object[])varArgs); //将String[]转换为Object[],长度为2的,错误

method.invoke(null, (Object)varArgs);//将整个String[] 转为Object,长度为1,符合

method.invoke(null, new Object[]{varArgs});//Object[]长度为1,正确。上一个和这个是等价的

什么时候使用可变长参数?

Stack Overflow上有个关于变长参数使用的问题。简单地说,

在不确定方法需要处理的对象的数量时可以使用可变长参数,会使得方法调用更简单,无需手动创建数组 new T[]{…} 。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值