自從1995年Java語言誕生到目前為止,已陪伴無數的程式設計人員走過這十年的光陰,尤其憑藉著Write Once, Run Anywhere的口號,便吸引了許多程式開發人員所注目的焦點,當然在這演變的過程之中,Java的版本也進步到了5.0(Tiger),但是不管是大改版或是小改版,要執行Java程式仍然是透過Java虛擬機器(JVM)(註一)去解析Java 位元碼(bytecode)來運作著,進而達成跨平台的實現,此種機制和Macromedia Flash有著異曲同工之處,為何筆者會如此認為呢?其實重點就在於Flash Player就相當於Java執行環境(JRE),而Flash SWF的檔案格式就好比如Java bytecode,兩者均依賴著中介語言來達成跨平台的機制,這也就是本文所要深入探討的重點之一。
本文會透過Java bytecode來剖析Varargs底層真實的面目,來了解這些底層的實作方式,有助於觀念的釐清,對於不管是在開發程式所在乎的效能決策上,或是想考取Java國際認證的朋友們,筆者相信本文都會提供一定的幫助!
何謂Varargs?
在我們開始探討Varargs功能之前,我們先來看一個JDK 5.0 所新增的功能之一 printf,如圖一所示,我們從API Document不難發現到printf方法(method)其實就已經運用Varargs這個新功能了!
圖一. 窺示printf API
Varargs(Variable-Length Argument Lists),從字面上顧名思義我們不難猜到Varargs代表著"變動長度參數列",筆者在這裡可以先向各位讀者透露,其實Varargs底層的處理方式就是陣列,我們稍待會一一來驗證!
開始剖析Varargs
在這裡我們先來看看一個取得字串陣列長度的方法(請參考程式一),可透過反組譯器(註二)來查看底層的bytecode。(請參考圖二)
程式一
public int getStringArrayLength(String arg[]) { return arg.length; }
圖二. getStringArrayLength方法的位元碼
剖析字串陣列的位元碼
從圖二的位元碼裡,我們可以明確地看到public int getStringArrayLength(java.lang.String[]);所定義傳入的參數是「一」個字串陣列,但是為何透過反組譯器所顯示出來的參數長度設定會等於2呢?(Args_size=2)這是因為我們的getStringArrayLength()方法是一個實體方法(instance method),所以第一個參數會以隱含(implicit)的方式傳入「this」,所以第一個參數就代表「this」,第二個參數才是代表字串陣列。
getStringArrayLength 方法的位元碼(bytecode):
0# aload_1 :: 將從區域變數(local variables)的索引位置「1」來載入物件參考(object reference)至Operand Stack。
說明:「在此就是用來取得參考到字串陣列的物件參考,來將它載入至Operand Stack」。
1# arraylength :: 取得陣列的長度。
說明:「會將參考到字串陣列的物件參考(object reference)從Operand Stack取出,並推入一個int整數至Operand Stack,此整數也就代表著陣列的長度」
2# ireturn :: 傳回int值。
說明:「也就是回傳在Operand Stack裡的陣列長度」
剖析Varargs的位元碼
看透了此方法的bytecode之後,我們再來看看Varargs的程式寫法(請參考程式二),然後再一次透過反組譯器來查閱底層的bytecode,我們赫然發現,果然使用Varargs的底層bytecode和使用陣列當參數的底層bytecode居然都是如出一徹地!(如圖二)從這裡我們就已經印證了,Varargs底層的實作方式就是陣列,但是它並不僅僅只是個單純的陣列取代!
程式二
public int getStringArrayLength(String... arg) { return arg.length; }
Varargs的特性與限制
Varargs仍然擁有屬於它自己的特性與限制:
Varargs允許傳遞零或一個以上相同型態的參數。(請參考程式三)
程式三
public static int getLength(String... arg) { return arg.length; } public static void main(String[] arg) { System.out.println("getLength() ==>"+getLength()); System.out.println("getLength() ==>"+getLength("one","two","thress")); }
使用Varargs必須放置在該方法的最後一個參數。(請參考程式四)
程式四
public int getLength(String... arg) //#正確 { return arg.length; } public int getLength(Integer count,String... arg) //#正確 { return arg.length; } public int getLength(String... arg,Integer count) //#錯誤 { return arg.length; }
一個方法裡最多只能定義一個Varargs。(請參考程式五)
程式五
public int getLength(String... arg) //#正確 { return arg.length; } public int getLength(String... arg,Integer... count) //#錯誤 { return arg.length; }
既然已知Varargs就是陣列
既然已經了解Varargs的底層就是陣列,所以我們也可以取而代之地將Java程式進入點改成Varargs的形式,而且也仍然可以使用陣列唯一的屬性變數「length」及陣列元素的存取!(請參考程式六)
程式六
public static void main(String... arg) { System.out.println("參數長度:"+arg.length); System.out.println("索引值[0] => "+arg[0]); }
在這裡有一點仍然必須注意一下,在方法多載(Overloading)的使用上,Java允許相同的方法名稱但各自擁有不同的參數列!說到這裡或許有一些讀者已經了解筆者的用意,沒錯!當我們使用陣列及Varargs在相同的方法名稱當參數列會發生什麼情形呢?(請參考程式七)
程式七
public int getLength(String... arg) { return arg.length; } public int getLength(String[] arg) { return arg.length; }
由於Varargs底層也是陣列的緣故,所以這時候就取決於看那一個方法宣告在最前面,以上述的程式碼為例,Java編譯器會告知我們getLength(java.lang.String...) is already defined in Test!
為何需要Varargs的存在?
現在我們換個角度來思考,那為何需要Varargs的存在呢?或許某些讀者會認為底層既然是陣列,那直接使用陣列不就好了!理論上來說是的確可以這麼做,但是既然在JDK 5.0加上Varargs這個功能,必然有它的優勢與存在的必要性,我們來看底下的程式八即可明確地了解Varargs所帶來的好處:
程式八
public static void NonVarargs(String[] str) { for(String s : str) System.out.print(s + " "); System.out.println(); } public static void Varargs(String... str) { for(String s : str) System.out.print(s + " "); System.out.println(); } public static void main(String[] arg) { System.out.print("呼叫NonVarargs method:"); NonVarargs(new String[]{"one","two","three"}); //#1 System.out.print("呼叫Varargs method:"); Varargs("one","two","three"); //#2 }
從上列的程式可以看出#2的程式明顯地簡化與簡單,但實際上使用Varargs仍然是透過#1的方式,
也就是說,上列的程式使用Varargs的方式時,便不需要再宣告一個字串陣列,而是透過Varargs去委託Java編譯器去做這層的轉換處理,所以我們只要將原始檔編譯成類別(Class)檔,剩下的就交給Java編譯器自動地去幫我們處理,從這裡就可以看得出來,JDK 5.0 提供了簡化程式設計人員更方便使用的語法,這也就是中介語言所帶來的極大好處之一!
剖析「傳遞零個參數至Varargs」
由於在本文前面有談到Varargs的特性之一「Varargs允許傳遞零或一個以上相同型態的參數」,在此我們直接來測試看看呼叫Varargs()方法會產生什麼樣的變化,請將上述的程式「Varargs("one","two","three"); //#2」 改成「Varargs();」即可,接著利用反組譯器來觀察Java bytecode,在觀察之後我們又可以發現到一項JDK 5.0的改革(如圖三)。
圖三. Varargs方法的部份位元碼
從bytecode之中可以明確地看到,在JDK 5.0之前的Java Compiler會自動地將需要用到串接字串的功能時,它會在底層運用StringBuffer來處理,但是在JDK 5.0之後都將改成StringBuilder來處理串接字串的功能,而由於StringBuilder 不是一個 thread safe 類別,所以在多執行緒的環境下必須注意到JDK 5.0的這項變動。
緊接著筆者就來剖析傳遞零個參數至Varargs時,究竟底層Java Compiler又是處理的呢?(如圖四)
圖四. main 方法的部份位元碼
38# iconst_0 :: 推入一個int整數至Operand Stack。
說明:「代表一個0整數推入到Operand Stack」
39# anewarray :: 建立一個參考型態的新陣列,並從Operand Stack取出一個整數當做陣列的長度,然後將參考到此陣列的arrayref再推入Operand Stack。
說明:「在此會建立一個長度為零的字串陣列」
42# invokestatic :: 呼叫類別(static)方法。
說明:「呼叫Varargs方法,並將字串陣列傳遞進去」
相信許多的讀者閱覽到此都已經豁然開朗了,從這裡我們可以完全地了解底層是如何處理「Varargs允許傳遞零或一個以上相同型態的參數」,簡而言之,上述的程式碼經由Java Compiler編譯之後,會將「Varargs();」看待成「Varargs(new String[0]);」來處理,這就是神奇的地方!^_^
#註一 = Java虛擬機器也可稱為堆疊機器(Stack Machine),表示著JVM底層的實作概念其實就是堆疊。
#註二 = JDK有提供一個javap反組譯器,可用來顯示位元碼(bytecode)。
#參考資料
1.The JavaTM Virtual Machine Specification Second Edition
2. Java 5.0 Tiger: A Developer's Notebook
本文若有任何謬誤,希望請不吝地賜教,若能指正不勝感激。
> javac -version
javac 1.6.0_16
我編譯
public static void Varargs(String... str) {}
public static void Varargs(String[] str) {}
並不會有任何的錯誤或警告,執行時也沒有
2009-11-30 14:53:16
@Henry
@@" 上述應該無法通過編譯才是,並出現:
「varargs.java:4: cannot declare both Varargs(java.lang.String[]) and Varargs(java.lang.String...) in varargs」
我的環境:javac 1.6.0_15
2009-11-30 15:24:45