A股上市公司传智教育(股票代码 003032)旗下技术交流社区北京昌平校区

 找回密码
 加入黑马

QQ登录

只需一步,快速开始

本帖最后由 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在运行时的类型为数组,可以通过反射来证明这一点:

  1. public static void f(Integer… i){}
  2. public static void main(String[] args) {
  3.         Method m = Test.class.getMethod("f", Integer[].class);
  4.         Class<?>[] paraTypes = m.getParameterTypes();
  5.         System.out.println(paraTypes[0].getName());
  6. }
复制代码

知道了vararg本质上是数组,它的访问方式和实参传递方式就一目了然了:
  1. public static void printArgs(Object... args) {
  2.     for(Object obj : args)
  3.         System.out.print(obj + " ");
  4. }
  5. public static void main(String[] args) {
  6.     printArgs(new String[] {"hello", "world"});
  7. }
复制代码

JDK1.5还提供了一种更灵活的实参传递方式——逗号分隔的参数列表:printArgs("hello", "world", 1, 'a'}); 逗号分隔的参数列表语法允许传入0个参数,也就是留空,比如printArgs(); 显然,这是Sun更推荐的方式,而下面将会看到,支持数组调用不过是兼容老版本代码的需要。
vararg的数组本质还决定了如下两个方法实际上是同一个方法:
  1. public static void f(int… i){}
  2. public static void f(int[] i){}
复制代码

因此,编译器不允许你同时定义这两个方法。但是它们之间有一个重要的区别:调用f(int[] i)时不能以逗号分隔的参数列表来传递实参。
vararg对JDK1.5之前版本的兼容
JDK1.5之前,不存在vararg语法,要实现可变长度参数,变通的办法是利用数组作为参数,例如:
  1. public static void printArgs(Object[] args) {
  2.     for(Object obj : args)
  3.         System.out.print(obj + " ");
  4. }
  5. public static int sum(int initVal, int[] numbers) {
  6.     int sum = initVal;
  7.     for(int n : numbers)
  8.         sum += n;
  9.     return sum;
  10. }
  11. public static void main(String[] args) {
  12.     printArgs(new String[] {"hello", "world"});
  13.     int sum = sum(0, new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10});
  14. }
复制代码

由于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。对此有两种解决方式:
  1. invoke(obj, (Object)new String[] {"hello", "world"}); // 声明我是一个Object,编译器你别拆了
  2. invoke(obj, new Object[] {new String[] {"hello", "world"}}); // 我加层壳,编译器你拆去吧
复制代码


其实main方法的String[] args也是1.5之前实现可变参数的遗迹,为了避免反射调用main方法时的麻烦,建议把main方法的参数列表写成String… args

带vararg方法的重载注意事项
其实这本来不是引入vararg后出现的问题,不过Thinking in Java里提到了,我就记录一下吧。

调用一个方法时,小括号内传递的实参实际上就是对形参赋值。Java要求形参的类型和个数与实参的类型和个数完全相同。对不带vararg的方法来说,参数个数是否相同很容易确定,麻烦的是参数类型是否相同。比如某个方法的签名是:
  1. 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开始尝试,而不是带着之前的转换结果继续尝试。

根据上述类型转换规则,下面这个调用是错误的:
  1. foo(int a) {}
  2. foo(float b) {}
  3. public static void main(String[] args) {
  4.     foo('a'); // 编译错误!!!
  5. }
复制代码

原因在于:'a'不能直接匹配任何一个foo方法的形参,但'a'经过类型提升后可以匹配任何一个foo方法的形参,导致编译器无法确定该调用哪个方法,于是报错。

使用vararg后,编译器往往无法从形参个数上去判断该调用哪个方法,类型判断就显得更为重要。下面的代码扩充了Thinking in Java中的例子:
  1. public class OverloadingVarargs2 {
  2. static void f(float i, Character... args) {
  3.     System.out.println("first");
  4. }
  5. static void f(Character... args) {
  6.     System.out.print("second");
  7. }
  8. static void f(Integer... args) {
  9.     System.out.print("third");
  10. }
  11. public static void main(String[] args) {
  12.     f(1, 'a'); // ok
  13.     f((char)1, 'a'); // 编译错误!!!
  14.     f('a', 'b'); // 编译错误!!!
  15.     f(1); // 编译错误!!!
  16. }
  17. }
复制代码

例子中编译错误的原因都是有多个匹配方法,编译器不知道该调用哪一个:
  •   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语言规范没有说尝试的顺序,我觉得原因在于从逻辑上来说,这里的顺序是无关紧要的:按照这个类型转换列表,给定某个形参的类型和传入的实参类型,如果某一种类型转换后形参和实参类型相同,则不可能有其他转换方式能够使得形参和实参类型相同。

点评

囧,发错版块了。。。  发表于 2014-7-26 14:46

1 个回复

倒序浏览
您需要登录后才可以回帖 登录 | 加入黑马