本文來自jarfIEld的博客,原文標題為《為什麼如此獲取Java數組的長度》。
記得vamcily 曾問我:“為什麼獲取數組的長度用.length(成員變量的形式),而獲取String的長度用.length()(成員方法的形式)?”
我當時一聽,覺得問得很有道理。做同樣一件事情,為什麼采用兩種風格迥異的風格呢?況且,Java中的數組其實是完備(full-fledged)的對象,直接暴露成員變量,可能不是一種很OO的風格。那麼,設計Java的那幫天才為什麼這麼做呢?
帶著這個疑問,我查閱了一些資料,主要是關於“JVM是如何處理數組”的。
數組對象的類是什麼?
既然數組都是對象,那麼數組的類究竟是什麼呢?當然不是Java.util.Arrays啦!我們以int一維數組為例,看看究竟。
public class Main {
public static void main(String args[]){
int a[] = new int[10]; Class clazz = a.getClass();
System.out.println(clazz.getName());
}
}
在SUN JDK 1.6上運行上述代碼,輸出為:
[I
看起來數組的類很奇怪,非但不屬於任何包,而且名稱還不是合法的標識符(identifIEr)。具體的命名規則[1]可以參見java.lang.Class.getName()的Javadoc。簡單的說,數組的類名由若干個'['和數組元素類型的內部名稱組成,'['的數目代表了數組的維度。
具有相同類型元素和相同維度的數組,屬於同一個類。如果兩個數組的元素類型相同,但維度不同,那麼它們也屬於不同的類。如果兩個數組的元素類型和維度均相同,但長度不同,那麼它們還是屬於同一個類。
數組的類有哪些成員呢?
既然我們知道了數組的類名是什麼,那麼就去看看數組的類究竟是什麼樣的吧?有哪些成員變量?有哪些成員方法?length這個成員變量在哪?是不是沒有length()這個成員方法?
找來找去,在JDK的代碼中沒有找打'[I'這個類。想想也對,'[I'都不是一個合法的標識符,肯定不會出現public class [I {...}這樣的Java代碼。我們暫且不管[I類是誰聲明的,怎麼聲明的,先用反射機制一探究竟吧。
public class Main {
public static void main(String[] args) {
int a[] = new int[10];
Class clazz = a.getClass();
System.out.println(clazz.getDeclaredFIElds().length);
System.out.println(clazz.getDeclaredMethods().length);
System.out.println(clazz.getDeclaredConstructors().length);
System.out.println(clazz.getDeclaredAnnotations().length);
System.out.println(clazz.getDeclaredClasses().length);
System.out.println(clazz.getSuperclass());
}
}
在SUN JDK 1.6上運行上述代碼,輸出為:
0 0 0 0 0 class Java.lang.Object
可見,[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查看字節碼。
public class Main {
public static void main(String[] args)
{ int a[] = new int[2]; int i = a.length;
}
}
使用SUN JDK 1.6編譯上述代碼,並使用jclasslib打開Main.class文件,得到main方法的字節碼:
0 iconst_2 //將int型常量2壓入操作數棧
1 newarray 10 (int) //將2彈出操作數棧,作為長度,創建一個元素類型為int, 維度為1的數組,並將數組的引用壓入操作數棧
3 astore_1 //將數組的引用從操作數棧中彈出,保存在索引為1的局部變量(即a)中
4 aload_1 //將索引為1的局部變量(即a)壓入操作數棧
5 arraylength //從操作數棧彈出數組引用(即a),並獲取其長度(JVM負責實現如何獲取),並將長度壓入操作數棧
6 istore_2 //將數組長度從操作數棧彈出,保存在索引為2的局部變量(即i)中
7 return //main方法返回
可見,在這段字節碼中,根本就沒有看見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的多維數組就不一定是規規矩矩的矩陣了,可以千變萬化。