企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
### [可变参数列表](https://lingcoder.gitee.io/onjava8/#/book/06-Housekeeping?id=%e5%8f%af%e5%8f%98%e5%8f%82%e6%95%b0%e5%88%97%e8%a1%a8) 你可以以一种类似 C 语言中的可变参数列表(C 通常把它称为"varargs")来创建和调用方法。这可以应用在参数个数或类型未知的场合。由于所有的类都最后继承于**Object**类(随着本书的进展,你会对此有更深的认识),所以你可以创建一个以 Object 数组为参数的方法,并像下面这样调用: ~~~ // housekeeping/VarArgs.java // Using array syntax to create variable argument lists class A {} public class VarArgs { static void printArray(Object[] args) { for (Object obj: args) { System.out.print(obj + " "); } System.out.println(); } public static void main(String[] args) { printArray(new Object[] {47, (float) 3.14, 11.11}); printArray(new Object[] {"one", "two", "three"}); printArray(new Object[] {new A(), new A(), new A()}); } } ~~~ 输出: ~~~ 47 3.14 11.11 one two three A@15db9742 A@6d06d69c A@7852e922 ~~~ `printArray()`的参数是**Object**数组,使用 for-in 语法遍历和打印数组的每一项。标准 Java 库能输出有意义的内容,但这里创建的是类的对象,打印出的内容是类名,后面跟着一个**@**符号以及多个十六进制数字。因而,默认行为(如果没有定义`toString()`方法的话,后面会讲这个方法)就是打印类名和对象的地址。 你可能看到像上面这样编写的 Java 5 之前的代码,它们可以产生可变的参数列表。在 Java 5 中,这种期盼已久的特性终于添加了进来,就像在`printArray()`中看到的那样: ~~~ // housekeeping/NewVarArgs.java // Using array syntax to create variable argument lists public class NewVarArgs { static void printArray(Object... args) { for (Object obj: args) { System.out.print(obj + " "); } System.out.println(); } public static void main(String[] args) { // Can take individual elements: printArray(47, (float) 3.14, 11.11); printArray(47, 3.14F, 11.11); printArray("one", "two", "three"); printArray(new A(), new A(), new A()); // Or an array: printArray((Object[]) new Integer[] {1, 2, 3, 4}); printArray(); // Empty list is OK } } ~~~ 输出: ~~~ 47 3.14 11.11 47 3.14 11.11 one two three A@15db9742 A@6d06d69c A@7852e922 1 2 3 4 ~~~ 有了可变参数,你就再也不用显式地编写数组语法了,当你指定参数时,编译器实际上会为你填充数组。你获取的仍然是一个数组,这就是为什么`printArray()`可以使用 for-in 迭代数组的原因。但是,这不仅仅只是从元素列表到数组的自动转换。注意程序的倒数第二行,一个**Integer**数组(通过自动装箱创建)被转型为一个**Object**数组(为了移除编译器的警告),并且传递给了`printArray()`。显然,编译器会发现这是一个数组,不会执行转换。因此,如果你有一组事物,可以把它们当作列表传递,而如果你已经有了一个数组,该方法会把它们当作可变参数列表来接受。 程序的最后一行表明,可变参数的个数可以为 0。当具有可选的尾随参数时,这一特性会有帮助: ~~~ // housekeeping/OptionalTrailingArguments.java public class OptionalTrailingArguments { static void f(int required, String... trailing) { System.out.print("required: " + required + " "); for (String s: trailing) { System.out.print(s + " "); } System.out.println(); } public static void main(String[] args) { f(1, "one"); f(2, "two", "three"); f(0); } } ~~~ 输出: ~~~ required: 1 one required: 2 two three required: 0 ~~~ 这段程序展示了如何使用除了**Object**类之外类型的可变参数列表。这里,所有的可变参数都是**String**对象。可变参数列表中可以使用任何类型的参数,包括基本类型。下面例子展示了可变参数列表变为数组的情形,并且如果列表中没有任何元素,那么转变为大小为 0 的数组: ~~~ // housekeeping/VarargType.java public class VarargType { static void f(Character... args) { System.out.print(args.getClass()); System.out.println(" length " + args.length); } static void g(int... args) { System.out.print(args.getClass()); System.out.println(" length " + args.length) } public static void main(String[] args) { f('a'); f(); g(1); g(); System.out.println("int[]: "+ new int[0].getClass()); } } ~~~ 输出: ~~~ class [Ljava.lang.Character; length 1 class [Ljava.lang.Character; length 0 class [I length 1 class [I length 0 int[]: class [I ~~~ `getClass()`方法属于 Object 类,将在"类型信息"一章中全面介绍。它会产生对象的类,并在打印该类时,看到表示该类类型的编码字符串。前导的**\[**代表这是一个后面紧随的类型的数组,**I**表示基本类型**int**;为了进行双重检查,我在最后一行创建了一个**int**数组,打印了其类型。这样也验证了使用可变参数列表不依赖于自动装箱,而使用的是基本类型。 然而,可变参数列表与自动装箱可以和谐共处,如下: ~~~ // housekeeping/AutoboxingVarargs.java public class AutoboxingVarargs { public static void f(Integer... args) { for (Integer i: args) { System.out.print(i + " "); } System.out.println(); } public static void main(String[] args) { f(1, 2); f(4, 5, 6, 7, 8, 9); f(10, 11, 12); } } ~~~ 输出: ~~~ 1 2 4 5 6 7 8 9 10 11 12 ~~~ 注意吗,你可以在单个参数列表中将类型混合在一起,自动装箱机制会有选择地把**int**类型的参数提升为**Integer**。 可变参数列表使得方法重载更加复杂了,尽管乍看之下似乎足够安全: ~~~ // housekeeping/OverloadingVarargs.java public class OverloadingVarargs { static void f(Character... args) { System.out.print("first"); for (Character c: args) { System.out.print(" " + c); } System.out.println(); } static void f(Integer... args) { System.out.print("second"); for (Integer i: args) { System.out.print(" " + i); } System.out.println(); } static void f(Long... args) { System.out.println("third"); } public static void main(String[] args) { f('a', 'b', 'c'); f(1); f(2, 1); f(0); f(0L); //- f(); // Won's compile -- ambiguous } } ~~~ 输出: ~~~ first a b c second 1 second 2 1 second 0 third ~~~ 在每种情况下,编译器都会使用自动装箱来匹配重载的方法,然后调用最明确匹配的方法。 但是如果调用不含参数的`f()`,编译器就无法知道应该调用哪个方法了。尽管这个错误可以弄清楚,但是它可能会使客户端程序员感到意外。 你可能会通过在某个方法中增加一个非可变参数解决这个问题: ~~~ // housekeeping/OverloadingVarargs2.java // {WillNotCompile} public class OverloadingVarargs2 { static void f(float i, Character... args) { System.out.println("first"); } static void f(Character... args) { System.out.println("second"); } public static void main(String[] args) { f(1, 'a'); f('a', 'b'); } } ~~~ **{WillNotCompile}**注释把该文件排除在了本书的 Gradle 构建之外。如果你手动编译它,会得到下面的错误信息: ~~~ OverloadingVarargs2.java:14:error:reference to f is ambiguous f('a', 'b'); \^ both method f(float, Character...) in OverloadingVarargs2 and method f(Character...) in OverloadingVarargs2 match 1 error ~~~ 如果你给这两个方法都添加一个非可变参数,就可以解决问题了: ~~~ // housekeeping/OverloadingVarargs3 public class OverloadingVarargs3 { static void f(float i, Character... args) { System.out.println("first"); } static void f(char c, Character... args) { System.out.println("second"); } public static void main(String[] args) { f(1, 'a'); f('a', 'b'); } } ~~~ 输出: ~~~ first second ~~~ 你应该总是在重载方法的一个版本上使用可变参数列表,或者压根不用它。