天天看點

【JVM】探究數組的本質

之前寫過一篇深入了解數組的博文 【Java核心技術卷】深入了解Java數組

, 這篇文章主要從理論的角度, 探讨了Java的數組。

這篇文章主要從實戰的角度去探究數組的本質。

在正文開始之前,我們有必要先關注一下類的加載機制:

在Java代碼中,類型的

加載

連接配接

初始化

過程都是在程式運作期間完成的

這裡的類型指的是我們定義的class interface,枚舉等等,這裡不涉及到對象的概念,是一種runtime的階段。這種加載機制提供了更大的靈活性,增加了更多的可能性。

簡單地說類型的加載 最常見的就是把位元組碼檔案從磁盤中加載到記憶體,連接配接就是将類與類之間的關系确定好,并且對位元組碼的一些處理,校驗等 也就是在這一階段完成了初始化 就是對類型中的靜态字段指派等等

具體流程如下:

類的加載、連接配接與初始化

  1. 加載:查找并加載類(class檔案)的二進制資料
  2. 連接配接

    ·-驗證:確定被加載的類的正确性(class檔案的格式等)

·-準備:為類的靜态變量配置設定記憶體,并将其初始化為預設值(準備階段 還沒有類的概念)

·-解析:把類中的符号引用轉換為直接引用

  1. 初始化:為類的靜态變量賦予正确的初始值

用圖示表述為

【JVM】探究數組的本質

Java程式對類的使用方式可分為兩種

  1. 主動使用
  2. 被動使用

所有的Java虛拟機實作必須在每個類或接口被Java程式“首次主動使用”時才初始化他們

不嚴格劃分,主動使用分為七種

  1. 建立類的執行個體
  2. 通路某個類或接口的靜态變量,或者對該靜态變量指派
  3. 調用類的靜态方法
  4. 反射(如Class.forName(“com.test.Test"))
  5. 初始化一個類的子類
  6. Java虛拟機啟動時被标明為啟動類的類(包含main方法的類)
  7. 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");
    }
}           

運作結果發現控制台沒有輸出任何東西

【JVM】探究數組的本質

沒有任何的輸出 證明Test類并沒有對TestValue類主動使用

我們已知的有兩點:
  1. 靜态代碼塊是在類加載時自動執行的,非靜态代碼塊是在建立對象時自動執行的代碼,不建立對象不執行該類的非靜态代碼塊。
  2. 所有的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");
    }
}           

列印結果:

【JVM】探究數組的本質

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");
    }
}           

運作直接報錯

【JVM】探究數組的本質

改變一下

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");
    }
}           
【JVM】探究數組的本質

是不是清晰多了??

二維數組與一維數組類似,我們測試一下二維數組

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());
    }
}           

結果:

【JVM】探究數組的本質

有很多人疑惑,為什麼學習底層? 相信,這篇文章已經告訴你答案了,這也是為什麼有的人寫的代碼,bug很少,遇見了也很快解決。有的人寫的代碼,bug不僅多,卻要花了大量的時間去debug的原因了。