1. 程式人生 > >java機制:類的載入詳解(靜態類,靜態變數,靜態方法,靜態程式碼塊,構造程式碼塊,成員變數,成員方法,父類...)

java機制:類的載入詳解(靜態類,靜態變數,靜態方法,靜態程式碼塊,構造程式碼塊,成員變數,成員方法,父類...)

       “程式碼編譯的結果從本地機器碼轉變為位元組碼,是儲存格式發展的一小步,卻是變成語言發展的一大步”,這句話出自《深入理解JAVA虛擬機器》

一.原始碼編譯

       程式碼編譯由JAVA原始碼編譯器來完成。主要是將原始碼編譯成位元組碼檔案(class檔案)。位元組碼檔案格式主要分為兩部分:常量池和方法位元組碼。

二.類的載入

      類的生命週期是從被載入到虛擬機器記憶體中開始,到卸載出記憶體結束。過程共有七個階段,其中到初始化之前的都是屬於類載入的部分.

      載入----驗證----準備----解析-----初始化----使用-----解除安裝

        系統可能在第一次使用某個類時載入該類,也可能採用預載入機制來載入某個類,當執行某個java程式時,會啟動一個java虛擬機器程序,兩次執行的java程式處於兩個不同的JVM程序中,兩個jvm之間並不會共享資料。

1、載入階段

這個流程中的載入是類載入機制中的一個階段,這兩個概念不要混淆,這個階段需要完成的事情有:

1)通過一個類的全限定名來獲取定義此類的二進位制位元組流。

2)將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構。

3)在java堆中生成一個代表這個類的Class物件,作為訪問方法區中這些資料的入口。

由於第一點沒有指明從哪裡獲取以及怎樣獲取類的二進位制位元組流,所以這一塊區域留給我開發者很大的發揮空間。這個我在後面的類載入器中在進行介紹。

2、準備階段

這個階段正式為類變數(被static修飾的變數)分配記憶體並設定類變數初始值,這個記憶體分配是發生在方法區中。

1、注意這裡並沒有對例項變數進行記憶體分配,例項變數將會在物件例項化時隨著物件一起分配在JAVA堆中。

2、這裡設定的初始值,通常是指資料型別的零值。

private static int a = 3;

 這個類變數a在準備階段後的值是0,將3賦值給變數a是發生在初始化階段。

3、初始化階段

初始化是類載入機制的最後一步,這個時候才正真開始執行類中定義的JAVA程式程式碼。在前面準備階段,類變數已經賦過一次系統要求的初始值,在初始化階段最重要的事情就是對類變數進行初始化,關注的重點是父子類之間各類資源初始化的順序。

java類中對類變數指定初始值有兩種方式:1、宣告類變數時指定初始值;2、使用靜態初始化塊為類變數指定初始值

參考 Java中類的載入時機

java虛擬機器規範雖然沒有強制性約束在什麼時候開始類載入過程,但是對於類的初始化,虛擬機器規範則嚴格規定了有且只有四種情況必須立即對類進行初始化,遇到new、getStatic、putStatic或invokeStatic這4條位元組碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。
生成這4條指令最常見的java程式碼場景是:

1)使用new關鍵字例項化物件;     通過反射建立例項Class.forName(“com.mengdd.Test”);

2)讀取一個類的靜態欄位(被final修飾、已在編譯期把結果放在常量池的靜態欄位除外)

3)設定一個類的靜態欄位(被final修飾、已在編譯期把結果放在常量池的靜態欄位除外)

4)呼叫一個類的靜態方法

驗證:

1)當類被初始化時,其靜態程式碼塊會執行。

class ClassLoadTime{

  static{

    System.out.println("ClassLoadTime類初始化時就會被執行!");

  }

  public ClassLoadTime(){

    System.out.println("ClassLoadTime建構函式!");

  }

}

class ClassLoadDemo{

  public static void main(String[] args){

    ClassLoadTime  clt = new ClassLoadTime();

  }

}

輸出結果:

 ClassLoadTime類初始化時就會被執行!

 ClassLoadTime建構函式!
2) 讀取一個類的靜態欄位(被final修飾、已在編譯期把結果放在常量池的靜態欄位除外)

class ClassLoadTime{

  static{

    System.out.println("ClassLoadTime類初始化時就會被執行!");

  }

  public static int max = 200; (防止測試類和此類不在一個包,使用public修飾符)

  public ClassLoadTime(){

    System.out.println("ClassLoadTime建構函式!");

  }

}

class ClassLoadDemo{

  public static void main(String[] args){

    int  value = ClassLoadTime.max;

    System.out.println(value);

  }

}

輸出:

ClassLoadTime類初始化時就會被執行!

200
3)設定一個類的靜態欄位(被final修飾、已在編譯期把結果放在常量池的靜態欄位除外)

class ClassLoadTime{

  static{

    System.out.println("ClassLoadTime類初始化時就會被執行!");

  }

  public static int max = 200; (防止測試類和此類不在一個包,使用public修飾符)

  public ClassLoadTime(){

    System.out.println("ClassLoadTime建構函式!");

  }

}

class ClassLoadDemo{

  public static void main(String[] args){

      ClassLoadTime.max = 100;

  }

}

輸出:

ClassLoadTime類初始化時就會被執行!
4)呼叫一個類的靜態方法

class ClassLoadTime{

  static{

    System.out.println("ClassLoadTime類初始化時就會被執行!");

  }

  public static int max = 200; (防止測試類和此類不在一個包,使用public修飾符)

  public ClassLoadTime(){

    System.out.println("ClassLoadTime建構函式!");

  }

  public static void method(){

    System.out.println("靜態方法的呼叫!");

  }

}

class ClassLoadDemo{

  public static void main(String[] args){

      ClassLoadTime.method();

  }

}

輸出:

ClassLoadTime類初始化時就會被執行!

靜態方法的呼叫!
被final修飾靜態欄位在操作使用時,不會使類進行初始化,因為在編譯期已經將此常量放在常量池。

測試:

class ClassLoadTime{

  static{

    System.out.println("ClassLoadTime類初始化時就會被執行!");

  }

  public static final int MIN = 10; (防止測試類和此類不在一個包,使用public修飾符)

}

class ClassLoadDemo{

  public static void main(String[] args){

   System.out.println(ClassLoadTime.MIN);

  }

}

輸出:

10
子類呼叫或者設定父類的靜態欄位或者呼叫父類的靜態方法時僅僅初始化父類,而不初始化子類。同樣讀取final修飾的常量不會進行類的初始化。

class Fu{

  public static int value = 20;

  static{

    System.out.println("父類進行了類的初始化!");

  }

}

class Zi{

  static{

    System.out.println("子類進行了類的初始化!");

  }

}

class LoadDemo{

  public static void main(String[] args){

    System.out.println(Zi.value);    

  }

}

輸出:

父類進行了類的初始化!

20
 java類中各種成員的初始化時機,此處不一一測試:

類變數(靜態變數)、例項變數(非靜態變數)、靜態程式碼塊、非靜態程式碼塊 的初始化時機:
* 由 static 關鍵字修飾的(如:類變數[靜態變數]、靜態程式碼塊)將在類被初始化建立例項物件之前被初始化,而且是按順序從上到下依次被執行;

   public static int value =34;

   static{

  System.out.println("靜態程式碼塊!");

 }

 public 類名(){    

  System.out.println("建構函式!");

 }

   一旦這樣寫,在類被初始化建立例項物件之前會先初始化靜態欄位value,然後執行靜態程式碼塊,當例項化物件時會執行構造方法中的程式碼
* 沒有 static 關鍵字修飾的(如:例項變數[非靜態變數]、非靜態程式碼塊)初始化實際上是會被提取到類的構造器中被執行的,但是會比類構造器中的
   程式碼塊優先執行到,其也是按順序從上到下依次被執行。

 public int value =34;

   {

  System.out.println("非靜態程式碼塊!");

 }

 public 類名(){    

  System.out.println("建構函式!");

 }

在使用建構函式例項化一個物件時,會先初始化value,然後執行非靜態程式碼塊,最後執行構造方法裡面的程式碼。

*在存在父類的時候,呼叫子類的構造時,會先呼叫父類的預設構造(空參構造),進行父類的初始化。

用final修飾某個類變數時,它的值在編譯時就已經確定好放入常量池了,所以在訪問該類變數時,等於直接從常量池中獲取,並沒有初始化該類。

初始化的步驟

1、如果該類還沒有載入和連線,則程式先載入該類並連線。

2、如果該類的直接父類沒有載入,則先初始化其直接父類。

3、如果類中有初始化語句,則系統依次執行這些初始化語句。

在第二個步驟中,如果直接父類又有直接父類,則系統會再次重複這三個步驟來初始化這個父類,依次類推,JVM最先初始化的總是java.lang.Object類。當程式主動使用任何一個類時,系統會保證該類以及所有的父類都會被初始化。

 

4.Static 類,成員變數,成員方法,程式碼塊,及載入順序

在《Java程式設計思想》P86頁有這樣一段話:

  “static方法就是沒有this的方法。在static方法內部不能呼叫非靜態方法,反過來是可以的。而且可以在沒有建立任何物件的前提下,僅僅通過類本身來呼叫static方法。這實際上正是static方法的主要用途。”

  這段話雖然只是說明了static方法的特殊之處,但是可以看出static關鍵字的基本作用,簡而言之,一句話來描述就是:

  方便在沒有建立物件的情況下來進行呼叫(方法/變數)。

  很顯然,被static關鍵字修飾的方法或者變數不需要依賴於物件來進行訪問,只要類被載入了,就可以通過類名去進行訪問。

  static可以用來修飾類的成員方法、類的成員變數,另外可以編寫static程式碼塊來優化程式效能。

一.Static方法,變數,程式碼塊

1)static方法

  static方法一般稱作靜態方法,由於靜態方法不依賴於任何物件就可以進行訪問,因此對於靜態方法來說,是沒有this的,因為它不依附於任何物件,既然都沒有物件,就談不上this了。並且由於這個特性,在靜態方法中不能訪問類的非靜態成員變數和非靜態成員方法,因為非靜態成員方法/變數都是必須依賴具體的物件才能夠被呼叫。

  但是要注意的是,雖然在靜態方法中不能訪問非靜態成員方法和非靜態成員變數,但是在非靜態成員方法中是可以訪問靜態成員方法/變數的。舉個簡單的例子:

在上面的程式碼中,由於print2方法是獨立於物件存在的,可以直接用過類名呼叫。假如說可以在靜態方法中訪問非靜態方法/變數的話,那麼如果在main方法中有下面一條語句:

  MyObject.print2();

  此時物件都沒有,str2根本就不存在,所以就會產生矛盾了。同樣對於方法也是一樣,由於你無法預知在print1方法中是否訪問了非靜態成員變數,所以也禁止在靜態成員方法中訪問非靜態成員方法。

  而對於非靜態成員方法,它訪問靜態成員方法/變數顯然是毫無限制的。

  因此,如果說想在不建立物件的情況下呼叫某個方法,就可以將這個方法設定為static。我們最常見的static方法就是main方法,至於為什麼main方法必須是static的,現在就很清楚了。因為程式在執行main方法的時候沒有建立任何物件,因此只有通過類名來訪問。

另外記住,即使沒有顯示地宣告為static,類的構造器實際上也是靜態方法。

2)static變數

  static變數也稱作靜態變數,靜態變數和非靜態變數的區別是:靜態變數被所有的物件所共享,在記憶體中只有一個副本,它當且僅當在類初次載入時會被初始化。而非靜態變數是物件所擁有的,在建立物件的時候被初始化,存在多個副本,各個物件擁有的副本互不影響。

  static成員變數的初始化順序按照定義的順序進行初始化。

3)static程式碼塊

  static關鍵字還有一個比較關鍵的作用就是 用來形成靜態程式碼塊以優化程式效能。static塊可以置於類中的任何地方,類中可以有多個static塊。在類初次被載入的時候,會按照static塊的順序來執行每個static塊,並且只會執行一次。

  為什麼說static塊可以用來優化程式效能,是因為它的特性:只會在類載入的時候執行一次。下面看個例子:


class Person{
    private Date birthDate;
     
    public Person(Date birthDate) {
        this.birthDate = birthDate;
    }
     
    boolean isBornBoomer() {
        Date startDate = Date.valueOf("1946");
        Date endDate = Date.valueOf("1964");
        return birthDate.compareTo(startDate)>=0 && birthDate.compareTo(endDate) < 0;
    }
}

isBornBoomer是用來這個人是否是1946-1964年出生的,而每次isBornBoomer被呼叫的時候,都會生成startDate和endDate兩個物件,造成了空間浪費,如果改成這樣效率會更好:


class Person{
    private Date birthDate;
    private static Date startDate,endDate;
    static{
        startDate = Date.valueOf("1946");
        endDate = Date.valueOf("1964");
    }
     
    public Person(Date birthDate) {
        this.birthDate = birthDate;
    }
     
    boolean isBornBoomer() {
        return birthDate.compareTo(startDate)>=0 && birthDate.compareTo(endDate) < 0;
    }
}

 因此,很多時候會將一些只需要進行一次的初始化操作都放在static程式碼塊中進行。

4)static class

Java中的類可以是static嗎?答案是可以。在java中我們可以有靜態例項變數、靜態方法、靜態塊。類也可以是靜態的。

     java允許我們在一個類裡面定義靜態類。比如內部類(nested class)。把nested class封閉起來的類叫外部類。在java中,我們不能用static修飾頂級類(top level class)。只有內部類可以為static。

     靜態內部類和非靜態內部類之間到底有什麼不同呢?下面是兩者間主要的不同。

    (1)內部靜態類不需要有指向外部類的引用。但非靜態內部類需要持有對外部類的引用。

    (2)非靜態內部類能夠訪問外部類的靜態和非靜態成員。靜態類不能訪問外部類的非靜態成員。他只能訪問外部類的靜態成員。

    (3)一個非靜態內部類不能脫離外部類實體被建立,一個非靜態內部類可以訪問外部類的資料和方法,因為他就在外部類裡面。

基於上面的討論,我們可以通過這些特性讓程式設計更簡單、有效。

/* 下面程式演示如何在java中建立靜態內部類和非靜態內部類 */
class OuterClass{
   private static String msg = "GeeksForGeeks";

   // 靜態內部類
   public static class NestedStaticClass{

       // 靜態內部類只能訪問外部類的靜態成員
       public void printMessage() {

         // 試著將msg改成非靜態的,這將導致編譯錯誤 
         System.out.println("Message from nested static class: " + msg); 
       }
    }

    // 非靜態內部類
    public class InnerClass{

       // 不管是靜態方法還是非靜態方法都可以在非靜態內部類中訪問
       public void display(){
          System.out.println("Message from non-static nested class: "+ msg);
       }
    }
} 

class Main
{
    // 怎麼建立靜態內部類和非靜態內部類的例項
    public static void main(String args[]){

       // 建立靜態內部類的例項
       OuterClass.NestedStaticClass printer = new OuterClass.NestedStaticClass();

       // 建立靜態內部類的非靜態方法
       printer.printMessage();   

       // 為了建立非靜態內部類,我們需要外部類的例項
       OuterClass outer = new OuterClass();        
       OuterClass.InnerClass inner  = outer.new InnerClass();

       // 呼叫非靜態內部類的非靜態方法
       inner.display();

       // 我們也可以結合以上步驟,一步建立的內部類例項
       OuterClass.InnerClass innerObject = new OuterClass().new InnerClass();

       // 同樣我們現在可以呼叫內部類方法
       innerObject.display();
    }
}

二.static關鍵字的誤區

1.static關鍵字會改變類中成員的訪問許可權嗎?

  有些初學的朋友會將java中的static與C/C++中的static關鍵字的功能混淆了。在這裡只需要記住一點:與C/C++中的static不同,Java中的static關鍵字不會影響到變數或者方法的作用域。在Java中能夠影響到訪問許可權的只有private、public、protected(包括包訪問許可權)這幾個關鍵字。看下面的例子就明白了:

提示錯誤"Person.age 不可視",這說明static關鍵字並不會改變變數和方法的訪問許可權。

2.能通過this訪問靜態成員變數嗎?

  雖然對於靜態方法來說沒有this,那麼在非靜態方法中能夠通過this訪問靜態成員變數嗎?先看下面的一個例子,這段程式碼輸出的結果是什麼?


public class Main {  
    static int value = 33;
 
    public static void main(String[] args) throws Exception{
        new Main().printValue();
    }
 
    private void printValue(){
        int value = 3;
        System.out.println(this.value);
    }
}

//----------------------------------------
輸出
33

這裡面主要考察隊this和static的理解。this代表什麼?this代表當前物件,那麼通過new Main()來呼叫printValue的話,當前物件就是通過new Main()生成的物件。而static變數是被物件所享有的,因此在printValue中的this.value的值毫無疑問是33。在printValue方法內部的value是區域性變數,根本不可能與this關聯,所以輸出結果是33。在這裡永遠要記住一點:靜態成員變數雖然獨立於物件,但是不代表不可以通過物件去訪問,所有的靜態方法和靜態變數都可以通過物件訪問(只要訪問許可權足夠)。

3.static能作用於區域性變數麼?

  在C/C++中static是可以作用域區域性變數的,但是在Java中切記:static是不允許用來修飾區域性變數。不要問為什麼,這是Java語法的規定。

三.常見的筆試面試題

 下面列舉一些面試筆試中經常遇到的關於static關鍵字的題目,僅供參考,如有補充歡迎下方留言。

1.下面這段程式碼的輸出結果是什麼?


public class Test extends Base{
 
    static{
        System.out.println("test static");
    }
     
    public Test(){
        System.out.println("test constructor");
    }
     
    public static void main(String[] args) {
        new Test();
    }
}
 
class Base{
     
    static{
        System.out.println("base static");
    }
     
    public Base(){
        System.out.println("base constructor");
    }
}

//--------------------------------------------------
輸出
base static
test static
base constructor
test constructor

至於為什麼是這個結果,我們先不討論,先來想一下這段程式碼具體的執行過程,在執行開始,先要尋找到main方法,因為main方法是程式的入口,但是在執行main方法之前,必須先載入Test類,而在載入Test類的時候發現Test類繼承自Base類,因此會轉去先載入Base類,在載入Base類的時候,發現有static塊,便執行了static塊。在Base類載入完成之後,便繼續載入Test類,然後發現Test類中也有static塊,便執行static塊。在載入完所需的類之後,便開始執行main方法。在main方法中執行new Test()的時候會先呼叫父類的構造器,然後再呼叫自身的構造器。因此,便出現了上面的輸出結果。

2.這段程式碼的輸出結果是什麼?


public class Test {
    Person person = new Person("Test");
    static{
        System.out.println("test static");
    }
     
    public Test() {
        System.out.println("test constructor");
    }
     
    public static void main(String[] args) {
        new MyClass();
    }
}
 
class Person{
    static{
        System.out.println("person static");
    }
    public Person(String str) {
        System.out.println("person "+str);
    }
}
 
 
class MyClass extends Test {
    Person person = new Person("MyClass");
    static{
        System.out.println("myclass static");
    }
     
    public MyClass() {
        System.out.println("myclass constructor");
    }
}


//--------------------------------------------------
輸出
test static
myclass static
person static
person Test
test constructor
person MyClass
myclass constructor

類似地,我們還是來想一下這段程式碼的具體執行過程。首先載入Test類,因此會執行Test類中的static塊。接著執行new MyClass(),而MyClass類還沒有被載入,因此需要載入MyClass類。在載入MyClass類的時候,發現MyClass類繼承自Test類,但是由於Test類已經被載入了,所以只需要載入MyClass類,那麼就會執行MyClass類的中的static塊。在載入完之後,就通過構造器來生成物件。而在生成物件的時候,必須先初始化父類的成員變數,因此會執行Test中的Person person = new Person(),而Person類還沒有被載入過,因此會先載入Person類並執行Person類中的static塊,接著執行父類的構造器,完成了父類的初始化,然後就來初始化自身了,因此會接著執行MyClass中的Person person = new Person(),最後執行MyClass的構造器。

 

3.這段程式碼的輸出結果是什麼?


public class Test {
     
    static{
        System.out.println("test static 1");
    }
    public static void main(String[] args) {
         
    }
     
    static{
        System.out.println("test static 2");
    }
}

//--------------------------------------
輸出
test static 1
test static 2

雖然在main方法中沒有任何語句,但是還是會輸出,原因上面已經講述過了。另外,static塊可以出現類中的任何地方(只要不是方法內部,記住,任何方法內部都不行),並且執行是按照static塊的順序執行的。