Java對象初始化詳解

出處:http://blog.jobbole.com/23939/java

 

在Java中,一個對象在能夠被使用以前必需要被正確地初始化,這一點是Java規範規定的。本文試圖對Java如何執行對象的初始化作一個詳細深 入地介紹(與對象初始化相同,類在被加載以後也是須要初始化的,本文在最後也會對類的初始化進行介紹,相對於對象初始化來講,類的初始化要相對簡單一 些)。編程

1.Java對象什麼時候被初始化app

Java對象在其被建立時初始化,在Java代碼中,有兩種行爲能夠引發對象的建立。其中比較直觀的一種,也就是一般所說的顯式對象建立,就是經過 new關鍵字來調用一個類的構造函數,經過構造函數來建立一個對象,這種方式在java規範中被稱爲「由執行類實例建立表達式而引發的對象建立」。
固然,除了顯式地建立對象,如下的幾種行爲也會引發對象的建立,可是並非經過new關鍵字來完成的,所以被稱做隱式對象建立,他們分別是:ide

● 加載一個包含String字面量的類或者接口會引發一個新的String對象被建立,除非包含相同字面量的String對象已經存在與虛擬機內了(JVM 會在內存中會爲全部碰到String字面量維護一份列表,程序中使用的相同字面量都會指向同一個String對象),好比,函數

1
2
3
4
class StringLiteral {
    private String str = "literal";
    private static String sstr = "s_literal";
}

● 自動裝箱機制可能會引發一個原子類型的包裝類對象被建立,好比,ui

1
2
3
class PrimitiveWrapper {
    private Integer iWrapper = 1;
}

● String鏈接符也可能會引發新的String或者StringBuilder對象被建立,同時還可能引發原子類型的包裝對象被建立,好比 (本人試了下,在mac ox下1.6.0_29版本的javac,對待下面的代碼會經過StringBuilder來完成字符串的鏈接,並無將i包裝成Integer,由於 StringBuilder的append方法有一個重載,其方法參數是int),this

1
2
3
4
5
6
7
public class StringConcatenation {
    private static int i = 1;
 
    public static void main(String... args) {
        System.out.println("literal" + i);
    }
}

2.Java如何初始化對象編碼

當一個對象被建立以後,虛擬機會爲其分配內存,主要用來存放對象的實例變量及其從超類繼承過來的實例變量(即便這些從超類繼承過來的實例變量有可能被隱藏也會被分配空間)。在爲這些實例變量分配內存的同時,這些實例變量也會被賦予默認值。spa

引用線程

關於實例變量隱藏

1
2
3
4
5
6
7
8
9
10
11
class Foo {
    int i = 0;
}
 
class Bar extends Foo {
    int i = 1;
    public static void main(String... args) {
        Foo foo = new Bar();
        System.out.println(foo.i);
    }
}

上面的代碼中,Foo和Bar中都定義了變量i,在main方法中,咱們用Foo引用一個Bar對象,若是實例變量與方法同樣,容許被覆蓋,那麼打印的結果應該是1,可是實際的結果確是0。
可是若是咱們在Bar的方法中直接使用i,那麼用的會是Bar對象本身定義的實例變量i,這就是隱藏,Bar對象中的i把Foo對象中的i給隱藏了,這條規則對於靜態變量一樣適用。

在內存分配完成以後,java的虛擬機就會開始對新建立的對象執行初始化操做,由於java規範要求在一個對象的引用可見以前須要對其進行初始化。在Java中,三種執行對象初始化的結構,分別是實例初始化器、實例變量初始化器以及構造函數。

2.1. Java的構造函數

每個Java中的對象都至少會有一個構造函數,若是咱們沒有顯式定義構造函數,那麼Java編譯器會爲咱們自動生成一個構造函數。構造函數與類中 定義的其餘方法基本同樣,除了構造函數沒有返回值,名字與類名同樣以外。在生成的字節碼中,這些構造函數會被命名成<init>方法,參數列 表與Java語言書寫的構造函數的參數列表相同(<init>這樣的方法名在Java語言中是非法的,可是對於JVM來講,是合法的)。另 外,構造函數也能夠被重載。

Java要求一個對象被初始化以前,其超類也必須被初始化,這一點是在構造函數中保證的。Java強制要求Object對象(Object是 Java的頂層對象,沒有超類)以外的全部對象構造函數的第一條語句必須是超類構造函數的調用語句或者是類中定義的其餘的構造函數,若是咱們即沒有調用其 他的構造函數,也沒有顯式調用超類的構造函數,那麼編譯器會爲咱們自動生成一個對超類構造函數的調用指令,好比,

1
2
3
public class ConstructorExample {
 
}

對於上面代碼中定義的類,若是觀察編譯以後的字節碼,咱們會發現編譯器爲咱們生成一個構造函數,以下,

1
2
3
aload_0
invokespecial    #8; //Method java/lang/Object."<init>":()V
return

上面代碼的第二行就是調用Object對象的默認構造函數的指令。

正由於如此,若是咱們顯式調用超類的構造函數,那麼調用指令必須放在構造函數全部代碼的最前面,是構造函數的第一條指令。這麼作才能夠保證一個對象在初始化以前其全部的超類都被初始化完成。

若是咱們在一個構造函數中調用另一個構造函數,以下所示,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ConstructorExample {
    private int i;
 
    ConstructorExample() {
        this(1);
        ....
    }
 
    ConstructorExample(int i) {
        ....
        this.i = i;
        ....
    }
}

對於這種狀況,Java只容許在ConstructorExample(int i)內出現調用超類的構造函數,也就是說,下面的代碼編譯是沒法經過的,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ConstructorExample {
    private int i;
 
    ConstructorExample() {
        super();
        this(1);
        ....
    }
 
    ConstructorExample(int i) {
        ....
        this.i = i;
        ....
    }
}

或者,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ConstructorExample {
    private int i;
 
    ConstructorExample() {
        this(1);
        super();
        ....
    }
 
    ConstructorExample(int i) {
        ....
        this.i = i;
        ....
    }
}

Java對構造函數做出這種限制,目的是爲了要保證一個類中的實例變量在被使用以前已經被正確地初始化,不會致使程序執行過程當中的錯誤。可是,與C 或者C++不一樣,Java執行構造函數的過程與執行其餘方法並無什麼區別,所以,若是咱們不當心,有可能會致使在對象的構建過程當中使用了沒有被正確初始 化的實例變量,以下所示,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Foo {
    int i;
 
    Foo() {
        i = 1;
        int x = getValue();
        System.out.println(x);
    }
 
    protected int getValue() {
        return i;
    }
}
 
class Bar extends Foo {
    int j;
 
    Bar() {
        j = 2;
    }
 
    @Override
    protected int getValue() {
        return j;
    }
}
 
public class ConstructorExample {
    public static void main(String... args) {
        Bar bar = new Bar();
    }
}

若是運行上面這段代碼,會發現打印出來的結果既不是1,也不是2,而是0。根本緣由就是Bar重載了Foo中的getValue方法。在執行Bar 的構造函數是,編譯器會爲咱們在Bar構造函數開頭插入調用Foo的構造函數的代碼,而在Foo的構造函數中調用了getValue方法。因爲Java對 構造函數的執行沒有作特殊處理,所以這個getValue方法是被Bar重載的那個getValue方法,而在調用Bar的getValue方法 時,Bar的構造函數尚未被執行,這個時候j的值仍是默認值0,所以咱們就看到了打印出來的0。

2.2. 實例變量初始化器與實例初始化器

咱們能夠在定義實例變量的同時,對實例變量進行賦值,賦值語句就時實例變量初始化器了,好比,

1
2
3
4
public class InstanceVariableInitializer {
    private int i = 1;
    private int j = i + 1;
}

若是咱們以這種方式爲實例變量賦值,那麼在構造函數執行以前會先完成這些初始化操做。

咱們還能夠經過實例初始化器來執行對象的初始化操做,好比,

1
2
3
4
5
6
7
8
9
public class InstanceInitializer {
 
    private int i = 1;
    private int j;
 
    {
        j = 2;
    }
}

上面代碼中花括號內代碼,在Java中就被稱做實例初始化器,其中的代碼一樣會先於構造函數被執行。

若是咱們定義了實例變量初始化器與實例初始化器,那麼編譯器會將其中的代碼放到類的構造函數中去,這些代碼會被放在對超類構造函數的調用語句以後 (還記得嗎?Java要求構造函數的第一條語句必須是超類構造函數的調用語句),構造函數自己的代碼以前。咱們來看下下面這段Java代碼被編譯以後的字 節碼,Java代碼以下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class InstanceInitializer {
 
    private int i = 1;
    private int j;
 
    {
        j = 2;
    }
 
    public InstanceInitializer() {
        i = 3;
        j = 4;
    }
}

編譯以後的字節碼以下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
aload_0
invokespecial    #11; //Method java/lang/Object."<init>":()V
aload_0
iconst_1
putfield #13; //Field i:I
aload_0
iconst_2
putfield #15; //Field j:I
aload_0
iconst_3
putfield #13; //Field i:I
aload_0
iconst_4
putfield #15; //Field j:I
return

上面的字節碼,第4,5行是執行的是源代碼中i=1的操做,第6,7行執行的源代碼中j=2的操做,第8-11行纔是構造函數中i=3和j=4的操做。

Java是按照編程順序來執行實例變量初始化器和實例初始化器中的代碼的,而且不容許順序靠前的實例初始化器或者實例變量初始化器使用在其後被定義和初始化的實例變量,好比,

1
2
3
4
5
6
7
8
9
10
11
12
13
public class InstanceInitializer {
    {
        j = i;
    }
 
    private int i = 1;
    private int j;
}
 
public class InstanceInitializer {
    private int j = i;
    private int i = 1;
}

上面的這些代碼都是沒法經過編譯的,編譯器會抱怨說咱們使用了一個未經定義的變量。之因此要這麼作,是爲了保證一個變量在被使用以前已經被正確地初始化。可是咱們仍然有辦法繞過這種檢查,好比,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class InstanceInitializer {
    private int j = getI();
    private int i = 1;
 
    public InstanceInitializer() {
        i = 2;
    }
 
    private int getI() {
        return i;
    }
 
    public static void main(String[] args) {
        InstanceInitializer ii = new InstanceInitializer();
        System.out.println(ii.j);
    }
}

若是咱們執行上面這段代碼,那麼會發現打印的結果是0。所以咱們能夠確信,變量j被賦予了i的默認值0,而不是通過實例變量初始化器和構造函數初始化以後的值。

引用

一個實例變量在對象初始化的過程當中會被賦值幾回?

在本文的前面部分,咱們提到過,JVM在爲一個對象分配完內存以後,會給每個實例變量賦予默認值,這個時候實例變量被第一次賦值,這個賦值過程是沒有辦法避免的。

若是咱們在實例變量初始化器中對某個實例x變量作了初始化操做,那麼這個時候,這個實例變量就被第二次賦值了。

若是咱們在實例初始化器中,又對變量x作了初始化操做,那麼這個時候,這個實例變量就被第三次賦值了。

若是咱們在類的構造函數中,也對變量x作了初始化操做,那麼這個時候,變量x就被第四次賦值。

也就是說,一個實例變量,在Java的對象初始化過程當中,最多能夠被初始化4次。

2.3. 總結

經過上面的介紹,咱們對Java中初始化對象的幾種方式以及經過何種方式執行初始化代碼有了瞭解,同時也對何種狀況下咱們可能會使用到未經初始化的變量進行了介紹。在對這些問題有了詳細的瞭解以後,就能夠在編碼中規避一些風險,保證一個對象在可見以前是徹底被初始化的。

3.關於類的初始化

Java規範中關於類在什麼時候被初始化有詳細的介紹,在3.0規範中的12.4.1節能夠找到,這裏就再也不多說 了。簡單來講,就是當類被第一次使用的時候會被初始化,並且只會被一個線程初始化一次。咱們能夠經過靜態初始化器和靜態變量初始化器來完成對類變量的初始 化工做,好比,

1
2
3
4
5
6
7
public class StaticInitializer {
    static int i = 1;
 
    static {
        i = 2;
    }
}

上面經過兩種方式對類變量i進行了賦值操做,分別經過靜態變量初始化器(代碼第2行)以及靜態初始化器(代碼第5-6行)完成。

靜態變量初始化器和靜態初始化器基本同實例變量初始化器和實例初始化器相同,也有相同的限制(按照編碼順序被執行,不能引用後定義和初始化的類變 量)。靜態變量初始化器和靜態初始化器中的代碼會被編譯器放到一個名爲static的方法中(static是Java語言的關鍵字,所以不能被用做方法 名,可是JVM卻沒有這個限制),在類被第一次使用時,這個static方法就會被執行。上面的Java代碼編譯以後的字節碼以下,咱們看到其中的 static方法,

1
2
3
4
5
6
7
8
static {};
  Code:
   Stack=1, Locals=0, Args_size=0
   iconst_1
   putstatic    #10; //Field i:I
   iconst_2
   putstatic    #10; //Field i:I
   return

在第2節中,咱們介紹了能夠經過特殊的方式來使用未經初始化的實例變量,對於類變量也一樣適用,好比,

1
2
3
4
5
6
7
8
9
10
11
12
public class StaticInitializer {
    static int j = getI();
    static int i = 1;
 
    static int getI () {
        return i;
    }
 
    public static void main(String[] args) {
        System.out.println(StaticInitializer.j);
    }
}

上面這段代碼的打印結果是0,類變量的值是i的默認值0。可是,因爲靜態方法是不能被覆寫的,所以第2節中關於構造函數調用被覆寫方法引發的問題不會在此出現。

相關文章
相關標籤/搜索