1. 程式人生 > >有關JVM處理Java陣列方法的思考

有關JVM處理Java陣列方法的思考

在Java中,獲取陣列的長度和String的長度是兩種不同的方法,這引起了本文作者的一番思考。本文從JVM的角度,探討了Java陣列在JVM中是什麼物件,有哪些成員,以及宣告方法。

本文來自jarfield的部落格,原文標題為《為什麼如此獲取Java陣列的長度》。

記得vamcily 曾問我:“為什麼獲取陣列的長度用.length(成員變數的形式),而獲取String的長度用.length()(成員方法的形式)?”

我當時一聽,覺得問得很有道理。做同樣一件事情,為什麼採用兩種風格迥異的風格呢?況且,Java中的陣列其實是完備(full-fledged)的物件,直接暴露成員變數,可能不是一種很OO的風格。那麼,設計Java的那幫天才為什麼這麼做呢?

帶著這個疑問,我查閱了一些資料,主要是關於“JVM是如何處理陣列”的。

陣列物件的類是什麼?

既然陣列都是物件,那麼陣列的類究竟是什麼呢?當然不是java.util.Arrays啦!我們以int一維陣列為例,看看究竟。

 
  1. public class Main {   
  2. public static void main(String args[]){   
  3. int a[] = new int[10]; Class clazz = a.getClass();   
  4. System.out.println(clazz.getName());   
  5. }   
  6. }  
  7.  

在SUN JDK 1.6上執行上述程式碼,輸出為:

[I

看起來陣列的類很奇怪,非但不屬於任何包,而且名稱還不是合法的識別符號(identifier)。具體的命名規則[1]可以參見java.lang.Class.getName()的javadoc。簡單的說,陣列的類名由若干個'['和陣列元素型別的內部名稱組成,'['的數目代表了陣列的維度。

具有相同型別元素和相同維度的陣列,屬於同一個類。如果兩個陣列的元素型別相同,但維度不同,那麼它們也屬於不同的類。如果兩個陣列的元素型別和維度均相同,但長度不同,那麼它們還是屬於同一個類。

陣列的類有哪些成員呢?

既然我們知道了陣列的類名是什麼,那麼就去看看陣列的類究竟是什麼樣的吧?有哪些成員變數?有哪些成員方法?length這個成員變數在哪?是不是沒有length()這個成員方法?

找來找去,在JDK的程式碼中沒有找打'[I'這個類。想想也對,'[I'都不是一個合法的識別符號,肯定不會出現public class [I {...}這樣的Java程式碼。我們暫且不管[I類是誰宣告的,怎麼宣告的,先用反射機制一探究竟吧。

 
  1. public class Main {   
  2. public static void main(String[] args) {   
  3. int a[] = new int[10]; 
  4. Class clazz = a.getClass();   
  5. System.out.println(clazz.getDeclaredFields().length);   
  6. System.out.println(clazz.getDeclaredMethods().length);   
  7. System.out.println(clazz.getDeclaredConstructors().length);   
  8. System.out.println(clazz.getDeclaredAnnotations().length);   
  9. System.out.println(clazz.getDeclaredClasses().length);   
  10. System.out.println(clazz.getSuperclass());   
  11. }   
  12. }  
  13.  

在SUN JDK 1.6上執行上述程式碼,輸出為:

 
  1. class java.lang.Object  
  2.  

可見,[I這個類是java.lang.Object的直接子類,自身沒有宣告任何成員變數、成員方法、建構函式和Annotation,可以說,[I就是個空類。我們立馬可以想到一個問題:怎麼連length這個成員變數都沒有呢?如果真的沒有,編譯器怎麼不報語法錯呢?想必編譯器對Array.length進行了特殊處理哇!

陣列的類在哪裡宣告的?

先不管為什麼沒有length成員變數,我們先搞清楚[I這個類是哪裡宣告的吧。既然[I都不是合法的識別符號,那麼這個類肯定在Java程式碼中顯式宣告的。想來想去,只能是JVM自己在執行時生成的了。JVM生成類還是一件很容易的事情,甚至無需生成位元組碼,直接在方法區中建立型別資料,就差不多完工了。

還沒有實力去看JVM的原始碼,於是翻了翻The JavaTM Virtual Machine Specification  Second Edition,果然得到了驗證,相關內容參考5.3.3 Creating Array Classes。

規範的描述很嚴謹,還摻雜了定義類載入器和初始化類載入器的內容。先不管這些,簡單概括一下:

類載入器先看看陣列類是否已經被建立了。如果沒有,那就說明需要建立陣列類;如果有,那就無需建立了。

如果陣列元素是引用型別,那麼類載入器首先去載入陣列元素的類。

JVM根據元素型別和維度,建立相應的陣列類。

呵呵,果然是JVM這傢伙自個偷偷建立了[I類。JVM不把陣列類放到任何包中,也不給他們起個合法的識別符號名稱,估計是為了避免和JDK、第三方及使用者自定義的類發生衝突吧。

再想想,JVM也必須動態生成陣列類,因為Java陣列類的數量與元素型別、維度(最多255)有關,相當相當多了,是沒法預先宣告好的。

居然沒有length這個成員變數!

我們已經發現,偷懶的JVM沒有為陣列類生成length這個成員變數,那麼Array.length這樣的語法如何通過編譯,如何執行的呢?

讓我們看看位元組碼吧!編寫一段最簡單的程式碼,使用jclasslib檢視位元組碼。

 
  1. public class Main {   
  2. public static void main(String[] args)   
  3. { int a[] = new int[2]; int i = a.length;   
  4. }   
  5. }  
  6.  

使用SUN JDK 1.6編譯上述程式碼,並使用jclasslib開啟Main.class檔案,得到main方法的位元組碼:

 
  1. 0 iconst_2                   //將int型常量2壓入運算元棧  
  2. 1 newarray 10 (int)    //將2彈出運算元棧,作為長度,建立一個元素型別為int, 維度為1的陣列,並將陣列的引用壓入運算元棧  
  3. 3 astore_1                 //將陣列的引用從運算元棧中彈出,儲存在索引為1的區域性變數(即a)中  
  4. 4 aload_1                  //將索引為1的區域性變數(即a)壓入運算元棧  
  5. 5 arraylength            //從運算元棧彈出陣列引用(即a),並獲取其長度(JVM負責實現如何獲取),並將長度壓入運算元棧  
  6. 6 istore_2                 //將陣列長度從運算元棧彈出,儲存在索引為2的區域性變數(即i)中  
  7. 7 return                    //main方法返回  
  8.  

可見,在這段位元組碼中,根本就沒有看見length這個成員變數,獲取陣列長度是由一條特定的指令arraylength實現(怎麼實現就不管了,JVM總有辦法)。編譯器對Array.length這樣的語法做了特殊處理,直接編譯成了arraylength指令。另外,JVM建立陣列類,應該就是由newarray這條指令觸發的了。

很自然地想到,編譯器也可以對Array.length()這樣的語法做特殊處理,直接編譯成arraylength指令。這樣的話,我們就可以使用方法呼叫的風格獲取陣列的長度了,這樣看起來貌似也更加OO一點。那為什麼不使用Array.length()的語法呢?也許是開發Java的那幫天才對.length有所偏愛,或者拋硬幣拍腦袋隨便決定的吧。 形式不重要,重要的是我們明白了背後的機理。

Array in Java

最後,對Java中純物件的陣列發表點感想吧。

 

相比C/C++中的陣列,Java陣列在安全性要好很多。C/C++常遇到的快取區溢位或陣列訪問越界的問題,在Java中不再存在。因為Java使用特定的指令訪問陣列的元素,這些指令都會對陣列的長度進行檢查。如果發現越界,就會丟擲java.lang.ArrayIndexOutOfBoundsException。

 

Java陣列元素的靈活性比較大。一個數組的元素本身也可以是陣列,只要所有元素的陣列型別相同即可。我們知道陣列的型別和長度無關,因此元素可以是長度不同的陣列。這樣,Java的多維陣列就不一定是規規矩矩的矩陣了,可以千變萬化。