Java可變引數
什麼是可變引數
- 在 Java 5 中提供了變長引數,允許在呼叫方法時傳入不定長度的引數。變長引數是Java的一個語法糖,本質上還是基於陣列的實現
void foo(String... args);
void foo(String[] args);
//方法簽名
([Ljava/lang/String;)V // public void foo(String[] args)
定義方法
在定義方法時,在最後一個形參後加上三點“…“,就表示該形參可以接受多個引數值,多個引數值被當成陣列傳入。上述定義有幾個要點需要注意:
可變引數只能作為函式的最後一個引數,但其前面可以有也可以沒有任何其他引數
由於可變引數必須是最後一個引數,所以一個函式最多隻能有一個可變引數
Java的可變引數,會被編譯器轉型為一個數組
變長引數在編譯為位元組碼後,在方法簽名中就是以陣列形態出現的。這兩個方法的簽名是一致的,不能作為方法的過載。如果同時出現,是不能編譯通過的。可變引數可以相容陣列,反之則不成立
public void foo(String...varargs){}
foo("arg1", "arg2", "arg3");
//上述過程和下面的呼叫是等價的
foo(new String[]{"arg1", "arg2", "arg3"});
- J2SE 1.5中新增了“泛型”的機制,可以在一定條件下把一個型別引數化。例如,可以在編寫一個類的時候,把一個方法的形參的型別用一個識別符號(如T)來代表, 至於這個識別符號到底表示什麼型別,則在生成這個類的例項的時候再行指定。這一機制可以用來提供更充分的程式碼重用和更嚴格的編譯時型別檢查。不過泛型機制卻不能和個數可變的形參配合使用。如果把一個能和不確定個實參相匹配的形參的型別,用一個識別符號來代表,那麼編譯器會給出一個“generic array creation”的錯誤
public class Varargs {
public static void test(String... args) {
for(String arg : args) {//當作陣列用foreach遍歷
System.out.println(arg);
}
}
//Compile error
//The variable argument type Object of the method must be the last parameter
//public void error1(String... args, Object o) {}
//public void error2(String... args, Integer... i) {}
//Compile error
//Duplicate method test(String...) in type Varargs
//public void test(String[] args){}
可變引數方法的呼叫
- 呼叫可變引數方法,可以給出零到任意多個引數,編譯器會將可變引數轉化為一個數組。也可以直接傳遞一個數組,示例如下
public class Varargs {
public static void test(String... args) {
for(String arg : args) {
System.out.println(arg);
}
}
public static void main(String[] args) {
test();//0個引數
test("a");//1個引數
test("a","b");//多個引數
test(new String[] {"a", "b", "c"});//直接傳遞陣列
}
}
方法過載
優先匹配固定引數
- 呼叫一個被過載的方法時,如果此呼叫既能夠和固定引數的過載方法匹配,也能夠與可變長引數的過載方法匹配,則選擇固定引數的方法
public class Varargs {
public static void test(String... args) {
System.out.println("version 1");
}
public static void test(String arg1, String arg2) {
System.out.println("version 2");
}
public static void main(String[] args) {
test("a","b");//version 2 優先匹配固定引數的過載方法
test();//version 1
}
}
匹配多個可變引數
- 呼叫一個被過載的方法時,如果此呼叫既能夠和兩個可變長引數的過載方法匹配,則編譯出錯
public class Varargs {
public static void test(String... args) {
System.out.println("version 1");
}
public static void test(String arg1, String... arg2) {
System.out.println("version 2");
}
public static void main(String[] args) {
test("a","b");//Compile error
}
}
方法重寫
避免帶有變長引數的方法過載
- 即便編譯器可以按照優先匹配固定引數的方式確定具體的呼叫方法,但在閱讀程式碼的依然容易掉入陷阱。要慎重考慮變長引數的方法過載
別讓null值和空值威脅到變長方法
public 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
}
}
- 修改如下:
public static void main(String[] args) {
Client client = new Client();
String[] strs = null;
client.methodA("China",strs);
}
- 讓編譯器知道這個null值是String型別的,編譯即可順利通過,也就減少了錯誤的發生
覆寫變長方法也要循規蹈矩
package com;
public class VarArgsTest2 {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
// 向上轉型
Base base = new Sub();
base.print("hello");
// 不轉型
Sub sub = new Sub();
sub.print("hello");//compile error
}
}
// 基類
class Base {
void print(String... args) {
System.out.println("Base......test");
}
}
// 子類,覆寫父類方法
class Sub extends Base {
@Override
void print(String[] args) {
System.out.println("Sub......test");
}
}
第一個能編譯通過,這是為什麼呢?事實上,base物件把子類物件sub做了向上轉型,形參列表是由父類決定的,當然能通過。而看看子類直接呼叫的情況,這時編譯器看到子類覆寫了父類的print方法,因此肯定使用子類重新定義的print方法,儘管引數列表不匹配也不會跑到父類再去匹配下,因為找到了就不再找了,因此有了型別不匹配的錯誤
這是個特例,覆寫的方法引數列表竟然可以與父類不相同,這違背了覆寫的定義,並且會引發莫名其妙的錯誤
這裡,總結下覆寫必須滿足的條件:
覆寫方法不能縮小訪問許可權
引數列表必須與被覆寫方法相同(包括顯示形式)
返回型別必須與被覆寫方法的相同或是其子類
覆寫方法不能丟擲新的異常,或者超出父類範圍的異常,但是可以丟擲更少、更有限的異常,或者不丟擲異常
可能出現的問題
使用 Object… 作為變長引數
public 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[], 可以作為一個物件陣列
反射方法呼叫時的注意事項
public 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異常,後兩個則正常呼叫
反射是執行時獲取的,在執行時看來,可變長引數和陣列是一致的,因而方法簽名為:
//方法簽名
([Ljava/lang/String;)V // public void foo(String[] varargs)
- 再來看一下 Method 物件的方法宣告:
Object invoke(Object obj, Object... args)
- args 雖然是一個可變長度的引數,但是 args 的長度是受限於該方法物件代表的真實方法的引數列表長度的,而從執行時簽名來看,([Ljava/lang/String;)V 實際上只有一個形參,即 String[] varargs,因而 invoke(Object obj, Object… args) 中可變引數 args 的實參長度只能為1
//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[]{…}