五分鐘學Java:可變引數究竟是怎麼一回事?

沉默王二發表於2019-12-30

在逛 programcreek 的時候,我發現了一些專注基礎但不容忽視的主題。比如說:Java 的可變引數究竟是怎麼一回事?像這類靈魂拷問的主題,非常值得深入地研究一下。

以前很不重視基礎,覺得不就那麼回事嘛,會用就行了。就比如說今天這個主題,管它可變不可變呢,不就是個引數嘛,還能有多大學問——抱著這種態度,我一直橫行江湖近十載(苦笑)。可等到讀者找我提一些基礎的問題時,我幾乎回答不上來,感覺知識是散的,或者是浮於表面的。幸好最近一段時間,我開始幡然醒悟,開始不放過任何一個細節,漸漸地,有點“知識儲備”了。

好了,牛逼吹完,讓我們來步入正題。Java 的可變引數究竟是怎麼一回事?

可變引數是 Java 1.5 的時候引入的功能,它允許方法使用任意多個、型別相同(is-a)的值作為引數。就像下面這樣。

public static void main(String[] args) {
    print("沉");
    print("沉""默");
    print("沉""默""王");
    print("沉""默""王""二");
}

public static void print(String... strs) {
    for (String s : strs)
        System.out.print(s);
    System.out.println();
}
複製程式碼

靜態方法 print() 就使用了可變引數,所以 print("沉") 可以,print("沉", "默") 也可以,甚至 3 個、 4 個或者更多個字串都可以作為引數傳遞給 print() 方法。

說到可變引數,我想起來阿里巴巴開發手冊上有這樣一條規約。

意思就是儘量不要使用可變引數,如果要用的話,可變引數必須要在引數列表的最後一位。既然坑位有限,只能在最後,那麼可變引數就只能有一個(悠著點,悠著點)。如果可變引數不在最後一位,IDE 就會提示對應的錯誤,如下圖所示。

那可變引數是怎麼工作的呢?

原理也很簡單。當使用可變引數的時候,實際上是先建立了一個陣列,該陣列的大小就是可變引數的個數,然後將引數放入陣列當中,再將陣列傳遞給被呼叫的方法

這就是為什麼可以使用陣列作為引數來呼叫帶有可變引數的方法的根本原因。程式碼如下所示。

public static void main(String[] args) {
    print(new String[]{"沉"});
    print(new String[]{"沉""默"});
    print(new String[]{"沉""默""王"});
    print(new String[]{"沉""默""王""二"});
}

public static void print(String... strs) {
    for (String s : strs)
        System.out.print(s);
    System.out.println();
}
複製程式碼

那如果方法的引數是一個陣列,然後像使用可變引數那樣去呼叫方法的時候,能行得通嗎?大家感興趣的話,不妨試一試(行不通,噓)。

那一般什麼時候使用可變引數呢?

可變引數,可變引數,顧名思義,當一個方法需要處理任意多個相同型別的物件時,就可以定義可變引數。Java 中有一個很好的例子,就是 String 類的 format() 方法,就像下面這樣。

System.out.println(String.format("年紀是: %d"18));
System.out.println(String.format("年紀是: %d 名字是: %s"18"沉默王二"));
複製程式碼

PS:%d 表示將整數格式化為 10 進位制整數,%s 表示輸出字串。

如果不使用可變引數,那需要格式化的引數就必須使用“+”號操作符拼接起來了。麻煩也就惹禍上身了。

在實際的專案程式碼中,開源包 slf4j.jar 的日誌輸出就經常要用到可變引數(log4j 就沒法使用可變引數,日誌中需要記錄多個引數時就痛苦不堪了)。就像下面這樣。

protected Logger logger = LoggerFactory.getLogger(getClass());
logger.debug("名字是{}", mem.getName());
logger.debug("名字是{},年紀是{}", mem.getName(), mem.getAge());
複製程式碼

檢視原始碼就可以發現,debug() 方法使用的可變引數。

public void debug(String format, Object... arguments);
複製程式碼

那在使用可變引數的時候有什麼注意事項嗎?

有的,有的。我們要避免過載帶有可變引數的方法——這樣很容易讓編譯器陷入自我懷疑中。

public static void main(String[] args) {
    print(null);
}

public static void print(String... strs) {
    for (String a : strs)
        System.out.print(a);
    System.out.println();
}

public static void print(Integer... ints) {
    for (Integer i : ints)
        System.out.print(i);
    System.out.println();
}
複製程式碼

這時候,編譯器完全不知道該呼叫哪個 print() 方法,print(String... strs) 還是 print(Integer... ints),傻傻分不清。

假如真的需要過載帶有可變引數的方法,就必須在呼叫方法的時候給出明確的指示,不要讓編譯器去猜。

public static void main(String[] args) {
    String [] strs = null;
    print(strs);

    Integer [] ints = null;
    print(ints);
}

public static void print(String... strs) {
}

public static void print(Integer... ints) {
}
複製程式碼

上面這段程式碼是可以編譯通過的。因為編譯器知道實參是 String 型別還是 Integer 型別,只不過為了執行時不丟擲 NullPointerException,兩個 print() 方法的內部要做好判空的操作。


好了,各位讀者朋友們,以上就是本文的全部內容了。能看到這裡的都是最優秀的程式設計師,升職加薪就是你了?。如果覺得不過癮,還想看到更多,我再給大家推薦幾篇。

Java 如何獲取陣列和字串的長度?length 還是 length()?

為什麼 Java 字串是不可變的?

Java 的 substring() 是如何工作的?

如何檢查Java陣列中是否包含某個值 ?

原創不易,如果覺得有點用的話,請不要吝嗇你手中點贊的權力;如果想要第一時間看到二哥更新的文章,請掃描下方的二維碼,關注沉默王二公眾號。我們下篇文章見!

相關文章