Thinking in Java(4-6) 可變參數列表(Varargs)

Thinking in Java(4-6) 可變參數列表(Varargs)
Photo by orbtal media / Unsplash

Java 在 SE5 之後引入了可變參數列表(varargs)功能,使得方法能夠接收不定數量的參數,從而提供了更大的靈活性。本篇文章將探討 varargs 的使用方式,以及它與自動裝箱機制和方法多載的交互作用。

可變參數列表的基礎

在 Java 引入 varargs 之前,如果想要模擬 C 語言中的可變參數列表,需要透過建立 Object[] 陣列作為方法參數:

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[]{
      new Integer(47), new Float(3.14), new Double(11.11)
    });
    printArray(new Object[]{"one", "two", "three" });
    printArray(new Object[]{new A(), new A(), new A()});
  }
} /* Output: (Sample)
47 3.14 11.11
one two three
A@1a46e30 A@3e25a5 A@19821f
*/

在上述程式中,printArray() 方法接受一個 Object[] 作為參數,透過 foreach 迴圈遍歷陣列中的元素。然而,這種方式需要在呼叫方法時顯式地建立陣列,語法較為繁瑣。

引入 varargs 的優勢

Java SE5 引入了 varargs 特性,允許方法接收不定數量的參數,而無需顯式地建立陣列。這使得方法的呼叫更為簡潔和直觀。

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(new Integer(47), new Float(3.14),
      new Double(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
  }
} /* Output: (75% match)
47 3.14 11.11
47 3.14 11.11
one two three
A@1bab50a A@c3c749 A@150bd4d
1 2 3 4
*/

在這個範例中,printArray() 方法的參數宣告為 Object... args,這表示該方法可以接受任意數量的 Object 型態參數。編譯器會自動將傳入的參數封裝成一個陣列,供方法內部使用。

可變參數列表的特性

傳遞零個或多個參數

可變參數列表允許我們傳遞零個或多個參數:

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);
  }
} /* Output:
required: 1 one
required: 2 two three
required: 0
*/

在上述程式中,trailing 參數是可變參數列表,可以接收零個或多個 String 型態的參數。

使用非 Object 型態的可變參數列表

可變參數列表可以使用任何型態,包括基本型態:

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());
  }
} /* Output:
class [Ljava.lang.Character; length 1
class [Ljava.lang.Character; length 0
class [I length 1
class [I length 0
int[]: class [I
*/

在這個例子中,f()g() 方法分別接受 Characterint 型態的可變參數列表。

自動裝箱與可變參數列表

可變參數列表與自動裝箱(autoboxing)機制可以協同工作,使得基本型態與其包裝類型之間的轉換更加順暢:

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(new Integer(1), new Integer(2));
    f(4, 5, 6, 7, 8, 9);
    f(10, new Integer(11), 12);
  }
} /* Output:
1 2
4 5 6 7 8 9
10 11 12
*/

在這個範例中,我們可以混合使用基本型態的數值和包裝類型的物件,編譯器會自動進行裝箱和拆箱操作。

可變參數列表與方法多載

可變參數列表的引入也對方法的多載(overloading)帶來了影響,需要注意可能的歧義問題:

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't compile -- ambiguous
  }
} /* Output:
first a b c
second 1
second 2 1
second 0
third
*/

在上述程式中,編譯器根據傳入參數的型態和數量,選擇最符合的多載方法。然而,當沒有傳遞參數時,編譯器無法確定應該調用哪一個方法,會導致編譯錯誤。

解決方法多載的歧義

為了解決上述的歧義問題,可以為方法增加非可變參數,讓編譯器能夠區分:

// {CompileTimeError} (Won't compile)

public class OverloadingVarargs2 {
  static void f(float i, Character... args) {
    System.out.println("first");
  }
  static void f(Character... args) {
    System.out.print("second");
  }
  public static void main(String[] args) {
    f(1, 'a');
    f('a', 'b');
  }
}

在這個範例中,兩個方法都有一個不同型態的非可變參數,編譯器可以根據傳入的參數型態進行正確的匹配。

結論

Java 的可變參數列表(varargs)為方法提供了更大的靈活性,允許接受不定數量的參數,並與自動裝箱機制協同工作。然而,在使用 varargs 與方法多載時,需要注意可能的歧義問題,透過增加非可變參數等方式來解決。