黑马程序员技术交流社区
标题:
张孝祥老师Java高新视频学习心得之可变长度参数
[打印本页]
作者:
fantacyleo
时间:
2014-7-26 14:44
标题:
张孝祥老师Java高新视频学习心得之可变长度参数
本帖最后由 fantacyleo 于 2014-7-26 14:42 编辑
先上第一篇地址:
张孝祥老师Java高新视频学习心得之可变长度参数
可变长度参数(vararg)是JDK1.5加入的语法新特性,对于这个特性,我先说三个方面,貌似结合泛型后还有些tricky的地方,等我看完泛型的视频后再回来补上:
vararg的语法和内部原理
vararg的定义语法我总结为2条:
声明一个vararg:类型名… 参数名
例如:int… numbers
将声明的可变参数置于方法形参列表的末尾
例如:public int sum(int initValue, int… numbers)
vararg在运行时的类型为数组,可以通过反射来证明这一点:
public static void f(Integer… i){}
public static void main(String[] args) {
Method m = Test.class.getMethod("f", Integer[].class);
Class<?>[] paraTypes = m.getParameterTypes();
System.out.println(paraTypes[0].getName());
}
复制代码
知道了vararg本质上是数组,它的访问方式和实参传递方式就一目了然了:
public static void printArgs(Object... args) {
for(Object obj : args)
System.out.print(obj + " ");
}
public static void main(String[] args) {
printArgs(new String[] {"hello", "world"});
}
复制代码
JDK1.5还提供了一种更灵活的实参传递方式——逗号分隔的参数列表:printArgs("hello", "world", 1, 'a'}); 逗号分隔的参数列表语法允许传入0个参数,也就是留空,比如printArgs(); 显然,这是Sun更推荐的方式,而下面将会看到,支持数组调用不过是兼容老版本代码的需要。
vararg的数组本质还决定了如下两个方法实际上是同一个方法:
public static void f(int… i){}
public static void f(int[] i){}
复制代码
因此,编译器不允许你同时定义这两个方法。但是它们之间有一个重要的区别:调用f(int[] i)时不能以逗号分隔的参数列表来传递实参。
vararg对JDK1.5之前版本的兼容
JDK1.5之前,不存在vararg语法,要实现可变长度参数,变通的办法是利用数组作为参数,例如:
public static void printArgs(Object[] args) {
for(Object obj : args)
System.out.print(obj + " ");
}
public static int sum(int initVal, int[] numbers) {
int sum = initVal;
for(int n : numbers)
sum += n;
return sum;
}
public static void main(String[] args) {
printArgs(new String[] {"hello", "world"});
int sum = sum(0, new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10});
}
复制代码
由于1.5开始的vararg本质上仍然是数组,因此1.5之前接受数组作为可变长度参数的API只需要在形参中把[]换成“…”即可完成转换,而处理参数的代码和调用方法的代码都不用改变。
本来事情到此就可以打住了,老代码可以被新JDK兼容了。然而,Sun偏偏为vararg的调用设计了新语法——逗号分隔的参数列表,这带来了语义上的模糊:比如printArgs(new String[] {"hello", "world"}),从老代码的设计来说,应该是认为传递了两个参数("hello", "world");可如果从”逗号分隔的参数列表“来看,只传递了一个参数,不兼容性又来了。
没办法,Sun只好再加入一项兼容性措施:如果调用一个vararg方法时传给vararg的实参是Object[]类型,则这个实参数组会被自动分拆,其元素会形成”逗号分隔的参数列表“,传递给方法。比如反射中用到的invoke方法,如果我们写invoke(obj, new String[] {"hello", "world"});则1.5以上的编译器会把String[]分拆成两个元素,从而我们实际上是通过反射调用了一个接收2个参数的方法。然而,如果我们要调用的方法只有一个String[]参数,比如main方法,刚才那条invoke语句马上就会因为参数个数不匹配而抛出IllegalArgumentException: wrong argument number。对此有两种解决方式:
invoke(obj, (Object)new String[] {"hello", "world"}); // 声明我是一个Object,编译器你别拆了
invoke(obj, new Object[] {new String[] {"hello", "world"}}); // 我加层壳,编译器你拆去吧
复制代码
其实main方法的String[] args也是1.5之前实现可变参数的遗迹,为了避免反射调用main方法时的麻烦,建议把main方法的参数列表写成String… args
带vararg方法的重载注意事项
其实这本来不是引入vararg后出现的问题,不过Thinking in Java里提到了,我就记录一下吧。
调用一个方法时,小括号内传递的实参实际上就是对形参赋值。Java要求形参的类型和个数与实参的类型和个数完全相同。对不带vararg的方法来说,参数个数是否相同很容易确定,麻烦的是参数类型是否相同。比如某个方法的签名是:
foo(Object obj)
复制代码
如果这样调用:foo(new Object()),那当然没问题。如果是这样:foo(new String("hi")),Java将尝试做如下可能的类型转换[1]:
基本类型向上转换,即byte-->short-->int-->long-->float-->double
引用类型基于继承体系的向上转换,比如任意对象-->Object Dog-->Animal
自动拆箱
自动装箱
如果某种转换后形参和实参类型相同,则运行时就将调用该方法。如果一种转换方式不成功,则继续尝试其他转换方式,但两次尝试之间是相互独立的:foo('a')将无法匹配方法:foo(Integer i); 你不能说:'a'先转换成int,int再自动装箱为Integer。当'a'转为int仍然不匹配Integer时,编译器就会尝试其他转换方式,而且仍然是从'a'本身的类型char开始尝试,而不是带着之前的转换结果继续尝试。
根据上述类型转换规则,下面这个调用是错误的:
foo(int a) {}
foo(float b) {}
public static void main(String[] args) {
foo('a'); // 编译错误!!!
}
复制代码
原因在于:'a'不能直接匹配任何一个foo方法的形参,但'a'经过类型提升后可以匹配任何一个foo方法的形参,导致编译器无法确定该调用哪个方法,于是报错。
使用vararg后,编译器往往无法从形参个数上去判断该调用哪个方法,类型判断就显得更为重要。下面的代码扩充了Thinking in Java中的例子:
public class OverloadingVarargs2 {
static void f(float i, Character... args) {
System.out.println("first");
}
static void f(Character... args) {
System.out.print("second");
}
static void f(Integer... args) {
System.out.print("third");
}
public static void main(String[] args) {
f(1, 'a'); // ok
f((char)1, 'a'); // 编译错误!!!
f('a', 'b'); // 编译错误!!!
f(1); // 编译错误!!!
}
}
复制代码
例子中编译错误的原因都是有多个匹配方法,编译器不知道该调用哪一个:
f(1, 'a'); 1经过基本类型提升可以变为float,'a'经过装箱可以变为Character,因此匹配f(float i, Character... args)。而这个方法调用与f(Character... args)和f(Integer... args) 都不匹配:1要经过向下类型转换和装箱才能变为Character;'a'要经过基本类型提升和装箱才能变为Integer
f((char)1, 'a'); (char) 1和'a'经过一次装箱可以变为Character,因此匹配f(Character... args) 同时,(char) 1经过基本类型提升可以变为float,因此又匹配 f(float i, Character... args)
f('a', 'b'); 'a'和'b'经过装箱可以变为Character,因此匹配f(Character... args) 同时,'a'经过基本类型提升可以变为float,因此又匹配 f(float i, Character... args)
f(1); 1经过装箱可以变为Integer,因此匹配f(Integer... args) ;同时,1经过基本类型提升可以变为float,而作为vararg的Character可以接收0个参数,因此又匹配f(float i, Character... args)
[1] Java语言规范没有说尝试的顺序,我觉得原因在于从逻辑上来说,这里的顺序是无关紧要的:按照这个类型转换列表,给定某个形参的类型和传入的实参类型,如果某一种类型转换后形参和实参类型相同,则不可能有其他转换方式能够使得形参和实参类型相同。
欢迎光临 黑马程序员技术交流社区 (http://bbs.itheima.com/)
黑马程序员IT技术论坛 X3.2