在網上看到了下面的一段代碼:
public class Test {<br /> static {<br /> _i = 20;<br /> }<br /> public static int _i = 10;</p><p> public static void main(String[] args) {<br /> System.out.println(_i);<br /> }<br />}
上述代碼會列印出什麼結果來呢?10還是20?本文將以此代碼為引子,著重討論一下靜態變數的初始化問題。
問題1:靜態變數如何初始化
Java類中可以定義一個static塊,用於靜態變數的初始化。如:
public class Test {<br /> public static int _i;<br /> static {<br /> _i = 10;<br /> }<br />}
當然最常用的初始化靜態變數的操作是在聲明變數時直接進行賦值操作。如:
public class Test {<br /> public static int _i = 10;<br />}
那麼上述兩例在本質上有什麼區別嗎?回答是沒有區別。兩例代碼編譯之後的位元組碼完全一致,通過 “javap -c”查看到的位元組碼如下:
public class Test extends java.lang.Object{
public static int _i;
public Test();
Code:
0: aload_0
1: invokespecial #1; //Method java/lang/Object."<init>":()V
4: return
static {};
Code:
0: bipush 10
2: putstatic #2; //Field _i:I
5: return
}
通過位元組碼還可以看出,當類的定義中不含有static塊時,編譯器會為該類提供一個預設的static塊。當然這是在含有靜態變數初始化操作的前提下。如果靜態變數沒有初始化操作,則編譯器不會為之提供預設的static塊。如:
public class Test {<br /> public static int _i;<br />}
其位元組碼的表現形式為:
public class Test extends java.lang.Object{
public static int _i;
public Test();
Code:
0: aload_0
1: invokespecial #1; //Method java/lang/Object."<init>":()V
4: return
}
由於靜態變數是通過賦值操作進行初始化的,因此可以通過靜態函數傳回值的方式為其初始化。如:
public class Test {<br /> public static int _i = init();</p><p> private static int init() {<br /> return 10;<br /> }<br />}
其本質與下面的代碼相同:
public class Test {<br /> public static int _i;<br /> static {<br /> _i = init();<br /> }</p><p> private static int init() {<br /> return 10;<br /> }<br />}
問題2:JDK如何處理static塊
類定義中可以存在多個static塊嗎?回答是可以。如:
public class Test {<br /> public static int _i;<br /> static {<br /> _i = 10;<br /> }</p><p> public static void main(String[] args) {<br /> }</p><p> static {<br /> _i = 20;<br /> }<br />}
此類編譯之後的位元組碼為:
public class Test extends java.lang.Object{
public static int _i;
public 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: return
static {};
Code:
0: bipush 10
2: putstatic #2; //Field _i:I
5: bipush 20
7: putstatic #2; //Field _i:I
10: return
}
觀察static{}部分可以看出,上例的代碼與下面的代碼效果一致:
public class Test {<br /> public static int _i;</p><p> public static void main(String[] args) {<br /> }</p><p> static {<br /> _i = 10;<br /> _i = 20;<br /> }<br />}
此例可以證明,不僅類定義中可以有多個static塊,而且在編譯時間編譯器會將多個static塊按照代碼的前後位置重新組合成一個static塊。
問題3:如何看待靜態變數的聲明
靜態變數存放在常量池之中。如何證明呢?如:
public class Test {<br /> public static int _i = 10;<br />}
使用“javap -c -verbose”查看其位元組碼的內容如下:
public class Test extends java.lang.Object
SourceFile: "Test.java"
minor version: 0
major version: 49
Constant pool:
const #1 = Method #4.#14; // java/lang/Object."<init>":()V
const #2 = Field #3.#15; // Test._i:I
const #3 = class #16; // Test
const #4 = class #17; // java/lang/Object
const #5 = Asciz _i;
const #6 = Asciz I;
const #7 = Asciz <init>;
const #8 = Asciz ()V;
const #9 = Asciz Code;
const #10 = Asciz LineNumberTable;
const #11 = Asciz <clinit>;
const #12 = Asciz SourceFile;
const #13 = Asciz Test.java;
const #14 = NameAndType #7:#8;// "<init>":()V
const #15 = NameAndType #5:#6;// _i:I
const #16 = Asciz Test;
const #17 = Asciz java/lang/Object;
{
public static int _i;
public Test();
Code:
Stack=1, Locals=1, Args_size=1
0: aload_0
1: invokespecial #1; //Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 2: 0
static {};
Code:
Stack=1, Locals=0, Args_size=0
0: bipush 10
2: putstatic #2; //Field _i:I
5: return
LineNumberTable:
line 3: 0
}
我們看到,常量池中const #2指向的就是Test._i,也就是靜態變數。靜態變數被儲存到常量池中的工作原理這裡不深入討論。在此需要注意的是:
- 靜態變數的聲明與初始化是兩個不同的操作;
- 靜態變數的聲明在編譯時間已經明確了記憶體的位置。
如:
public class Test {<br /> public static int _i = 10;<br />}
上述代碼的本質可以視為:
public class Test {<br /> // 靜態變數的聲明<br /> public static int _i;</p><p> // 靜態變數的初始化<br /> static {<br /> _i = 10;<br /> }<br />}
由於靜態變數的聲明在編譯時間已經明確,所以靜態變數的聲明與初始化在編碼順序上可以顛倒。也就是說可以先編寫初始化的代碼,再編寫聲明代碼。如:
public class Test {<br /> // 靜態變數的初始化<br /> static {<br /> _i = 10;<br /> }</p><p> // 靜態變數的聲明<br /> public static int _i;<br />}
對初始問題的解答
解答了上述三個問題,讓我們再來看看開篇提到的問題。代碼如下:
public class Test {<br /> static {<br /> _i = 20;<br /> }<br /> public static int _i = 10;</p><p> public static void main(String[] args) {<br /> System.out.println(_i);<br /> }<br />}
其本質可以用下面的代碼錶示:
public class Test {<br /> static {<br /> _i = 20;<br /> }<br /> public static int _i;<br /> static {<br /> _i = 10;<br /> }</p><p> public static void main(String[] args) {<br /> System.out.println(_i);<br /> }<br />}
再簡化一下,可以表示為:
public class Test {<br /> public static int _i;</p><p> static {<br /> _i = 20;<br /> _i = 10;<br /> }</p><p> public static void main(String[] args) {<br /> System.out.println(_i);<br /> }<br />}
至此,代碼已經明確告訴我們列印結果是什麼了!