之前寫過一篇深入了解數組的博文 【Java核心技術卷】深入了解Java數組
, 這篇文章主要從理論的角度, 探讨了Java的數組。
這篇文章主要從實戰的角度去探究數組的本質。
在正文開始之前,我們有必要先關注一下類的加載機制:
在Java代碼中,類型的
加載
、 連接配接
與 初始化
過程都是在程式運作期間完成的 這裡的類型指的是我們定義的class interface,枚舉等等,這裡不涉及到對象的概念,是一種runtime的階段。這種加載機制提供了更大的靈活性,增加了更多的可能性。
簡單地說類型的加載 最常見的就是把位元組碼檔案從磁盤中加載到記憶體,連接配接就是将類與類之間的關系确定好,并且對位元組碼的一些處理,校驗等 也就是在這一階段完成了初始化 就是對類型中的靜态字段指派等等
具體流程如下:
類的加載、連接配接與初始化
- 加載:查找并加載類(class檔案)的二進制資料
-
連接配接
·-驗證:確定被加載的類的正确性(class檔案的格式等)
·-準備:為類的靜态變量配置設定記憶體,并将其初始化為預設值(準備階段 還沒有類的概念)
·-解析:把類中的符号引用轉換為直接引用
- 初始化:為類的靜态變量賦予正确的初始值
用圖示表述為

Java程式對類的使用方式可分為兩種
- 主動使用
- 被動使用
所有的Java虛拟機實作必須在每個類或接口被Java程式“首次主動使用”時才初始化他們
不嚴格劃分,主動使用分為七種
- 建立類的執行個體
- 通路某個類或接口的靜态變量,或者對該靜态變量指派
- 調用類的靜态方法
- 反射(如Class.forName(“com.test.Test"))
- 初始化一個類的子類
- Java虛拟機啟動時被标明為啟動類的類(包含main方法的類)
- JDK1.7開始提供的動态語言支援:java.lang.invoke.MethodHandle執行個體的解析結果REF_getStatic,REF_putStatic,REF_invokeStatic句柄對應的類沒有初始化,則初始化
除了以上七種情況,其他使用Java類的方式都被看作是對類的被動使用,都不會導緻類的初始化
下面我們試圖探究數組的本質
先看一個例子:
public class Test {
public static void main(String[] args) {
TestValue[] testValues = new TestValue[10];
}
}
class TestValue{
static {
System.out.println("TestValue static code");
}
}
運作結果發現控制台沒有輸出任何東西
沒有任何的輸出 證明Test類并沒有對TestValue類主動使用
我們已知的有兩點:
- 靜态代碼塊是在類加載時自動執行的,非靜态代碼塊是在建立對象時自動執行的代碼,不建立對象不執行該類的非靜态代碼塊。
- 所有的Java虛拟機實作必須在每個類或接口被Java程式“首次主動使用”時才初始化他們。
你可能會有疑問了 我們這裡都new出來一個TestValue[ ] 的執行個體啊。我們看看它的類型
package com.leetcodePractise.tstudy;
public class Test {
public static void main(String[] args) {
TestValue[] testValues = new TestValue[10];
System.out.println(testValues.getClass());
}
}
class TestValue{
static {
System.out.println("TestValue static code");
}
}
列印結果:
TestValue是我們生成的數組從屬的類型,其實這是Java虛拟機幫助我們在運作期聲明出來的,但是我們卻沒有在代碼中顯式聲明出來這種類型。
以下面的測試為例:
public class Test {
public static void main(String[] args) {
TestValue[] testValues = new TestValue[10];
}
}
class TestValue{
static {
System.out.println("TestValue static code");
}
}
反編譯Test.class
Compiled from "Test.java"
public class com.leetcodePractise.tstudy.Test {
public com.leetcodePractise.tstudy.Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: bipush 10
2: anewarray #2 // class com/leetcodePractise/tstudy/TestValue
5: astore_1
6: return
}
aload_0 将第一個引用類型本地變量推送至棧頂
anewarray 助記符表示建立一個引用型(如類、接口、數組)的數組,并将其引用值壓入棧頂
astore_1表示将棧頂引用型數值存入第二個本地變量
重點關注anewarray ,這裡僅僅是建立一個引用型的數組。這個數組的類型在運作期确定為TestValue類型,這是一個引用型的資料,并沒有将TestValue類進行加載,是以不會有static靜态代碼塊的執行
不相信的話,看下面的結果
public class Test {
public static void main(String[] args) {
TestValue[] testValues = new TestValue[10];
testValues[0].dosomething();
}
}
class TestValue{
static {
System.out.println("TestValue static code");
}
public void dosomething(){
System.out.println("haha");
}
}
運作直接報錯
改變一下
public class Test {
public static void main(String[] args) {
TestValue[] testValues = new TestValue[10];
testValues[0] = new TestValue();
testValues[0].dosomething();
}
}
class TestValue{
static {
System.out.println("TestValue static code");
}
public void dosomething(){
System.out.println("haha");
}
}
是不是清晰多了??
二維數組與一維數組類似,我們測試一下二維數組
TestValue[][] testValues2 = new TestValue[10][10];
System.out.println(testValues2.getClass());
列印結果是:
class [[Lcom.leetcodePractise.tstudy.TestValue;
這裡有兩個左中括号以示二維數組
探究數組對象的父類
//進一步探究數組對象的父類型
System.out.println(testValues.getClass().getSuperclass());
System.out.println(testValues2.getClass().getSuperclass());
列印結果均為class java.lang.Object
對于數組執行個體來說,其類型是由JVM在運作期動态生成的,表示為[Lcom leetcodePractise tstudy. Testvalue這種形式。
動态生成的類型,其父類型就是 Object
對于數組來說, JavaDoc經常将構成數組的元素為 Component,實際上就是将數組降低一個次元後的類型。
對下面這段代碼編譯成的位元組碼檔案進行反編譯
package com.leetcodePractise.tstudy;
public class Test {
public static void main(String[] args) {
TestValue[] testValues = new TestValue[10];
System.out.println(testValues.getClass());
TestValue[][] testValues2 = new TestValue[10][10];
System.out.println(testValues2.getClass());
}
}
class TestValue{
static {
System.out.println("TestValue static code");
}
}
反編譯結果
Compiled from "Test.java"
public class com.leetcodePractise.tstudy.Test {
public com.leetcodePractise.tstudy.Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: bipush 10
2: anewarray #2 // class com/leetcodePractise/tstudy/TestValue
5: astore_1
6: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
9: aload_1
10: invokevirtual #4 // Method java/lang/Object.getClass:()Ljava/lang/Class;
13: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
16: bipush 10
18: bipush 10
20: multianewarray #6, 2 // class "[[Lcom/leetcodePractise/tstudy/TestValue;"
24: astore_2
25: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
28: aload_2
29: invokevirtual #4 // Method java/lang/Object.getClass:()Ljava/lang/Class;
32: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
35: return
}
anewarray 助記符表示建立一個引用型(如類、接口、數組)的數組,并将其引用值壓入棧頂
aload_1 将第二個引用類型本地變量推送至棧頂
multianewarray 建立指定類型和指定次元的多元數組(執行指令時,操作棧中必須包含各次元的長度值),并将其引用壓入棧頂
astore_2表示将棧頂引用型數值存入第三個本地變量
aload_2 将第三個引用類型本地變量推送至棧頂
複盤一下吧:
數組對象的類型是在運作期确定下來的,這個過程并沒有主動使用運作期确定下來的類,是以不會引起類的加載。如果要想通過索引使用對象,還需要new出類相應的執行個體。
數組的本質已經探讨過了,因為數組對象的類型是在運作期确定下來的,這也留下了一個包袱,就是數組協變。
這裡也提一下吧
下面就是示範:
class Fruit {}
class Apple extends Fruit {}
class Jonathan extends Apple {}
class Orange extends Fruit {}
public class CovariantArrays {
public static void main(String[] args) {
Fruit[] fruit = new Apple[10];
fruit[0] = new Apple(); // OK
fruit[1] = new Jonathan(); // OK
// Runtime type is Apple[], not Fruit[] or Orange[]:
try {
// Compiler allows you to add Fruit:
//編譯時通過,編譯時Fruit[]數組可以裝入Fruit及其子類
//運作時Apple[]數組可以裝入Apple及其子類,
//運作時異常,運作時Apple[]數組不可以裝入Fruit類
fruit[0] = new Fruit(); // ArrayStoreException
} catch(Exception e) { System.out.println(e); }
try {
// Compiler allows you to add Oranges:
fruit[0] = new Orange(); // ArrayStoreException
} catch(Exception e) { System.out.println(e); }
}
}
上面的注釋非常詳細
fruit[0] = new Fruit(); // ArrayStoreException
因為數組對象的類型是在運作期确定下來的,此時的fruit[0]的類型是Apple類型的。
結果new出來的是它的父類肯定會報錯
為了嚴謹起見,我們測試一下
class Fruit {}
class Apple extends Fruit {}
class Jonathan extends Apple {}
class Orange extends Fruit {}
public class CovariantArrays {
public static void main(String[] args) {
Fruit[] fruit = new Apple[10];
fruit[0] = new Apple(); // OK
fruit[1] = new Jonathan(); // OK
System.out.println(fruit[0].getClass());
System.out.println(fruit[1].getClass());
}
}
結果:
有很多人疑惑,為什麼學習底層? 相信,這篇文章已經告訴你答案了,這也是為什麼有的人寫的代碼,bug很少,遇見了也很快解決。有的人寫的代碼,bug不僅多,卻要花了大量的時間去debug的原因了。