導引
作為java開發(fā)者算途,我們都知道內(nèi)部類分為靜態(tài)內(nèi)部類和非靜態(tài)內(nèi)部類履婉。非靜態(tài)內(nèi)部類不能有靜態(tài)成員(不包括靜態(tài)常量),但關于為什么不能有靜態(tài)成員婆排,網(wǎng)上有很多不同的聲音声旺。這里我先不說自己的結論,先看真實的運行情況段只。
預熱
1 非靜態(tài)內(nèi)部類是否可以包含靜態(tài)常量
這個問題可能有些伙伴還沒搞清楚腮猖,首先結論是可以。這里的靜態(tài)常量只包括基本類型和字符串字面值赞枕。
比如下面的代碼:
public class OutClass {
String name;
public class InClass {
public static final int AGE = 9;
public static final String NAME = "sollian";
}
}
public class TestOutIn {
public static void main(String[] args) {
Integer age = OutClass.InClass.AGE;
String name = OutClass.InClass.NAME;
}
}
然后我們看下反編譯TestOutIn.class
的結果:
public class TestOutIn {
public TestOutIn() {
}
public static void main(String[] args) {
Integer age = 9;
String name = "sollian";
}
}
可以看到靜態(tài)常量的引用會直接替換成它的值澈缺。這樣就不依賴類的初始化,因為使用的是值炕婶,跟類無關姐赡。
而其他的數(shù)據(jù)類型或者其他形式的常量依賴于類的初始化,比如:
public static final Object obj = new Object();
public static final int COUNT = Math.max(1, 2);
public static final int COUNT_RANDOM = Random.nextInt();
首先對于obj
柠掂,不可能把使用它的地方替換成new Object()
项滑。
關于COUNT
可能有的同學會說,可以把用到COUNT
的地方替換成Math.max(1,2)
涯贞,乍一看確實沒問題枪狂,但是可以思考一下:編譯期間,編譯器不知道Math.max(1,2)
的最終結果是什么宋渔,這是運行時做的事州疾;對編譯器而言,Math.max(1,2)
和Random.nextInt()
沒有區(qū)別皇拣,但顯然我們不能用Random.nextInt()
來替換COUNT_RANDOM
严蓖。
以上是從邏輯上的分析。在class文件中,可以簡單理解為颗胡,可以用值替換引用的靜態(tài)常量必須存在于類的常量池中毫深,比如上面提到的AGE
和NAME
:
#5 = Utf8 AGE
#6 = Utf8 I
#7 = Utf8 ConstantValue
#8 = Integer 9
#9 = Utf8 NAME
#10 = Utf8 Ljava/lang/String;
#11 = String #30 // sollian
關于如何查看類的字節(jié)碼,可以參考如下命令:
javap -c -l -p -v *.class
注:下文提到的靜態(tài)常量均是指放到常量池中的成員
知識點預熱結束杭措。上面我們提到靜態(tài)常量的使用不依賴類的初始化费什。關于類的生命周期,可以參考Java-類的生命周期淺析或其他文章手素。
類初始化測試
了解了類的生命周期后鸳址,我們知道類加載的最終產(chǎn)物是在堆中創(chuàng)建類對象,即Class泉懦。類初始化是執(zhí)行類的初始化語句稿黍,包括靜態(tài)量的初始化,執(zhí)行靜態(tài)代碼塊崩哩。
對于下面的代碼巡球,我們測試一下哪些調(diào)用不會觸發(fā)類初始化:
public class OutClass {
public static final int AGE = 320;
public static final String SEX = "male";
public static final String NAME = new String("sollian");
static {
System.out.println("OutClass初始化");
}
public static void show() {
}
}
測試代碼:
public class Test {
public static void main(String[] args) throws Exception {
noClassInit();
// classInit();
}
/**
* 不會觸發(fā)類的初始化
*/
private static void noClassInit() throws Exception {
System.out.println(OutClass.AGE);
System.out.println(OutClass.SEX);
System.out.println(OutClass.class);
Class<OutClass> clazz = OutClass.class;
Method method = clazz.getDeclaredMethod("show");
System.out.println(method);
}
/**
* 會觸發(fā)類的初始化
*/
private static void classInit() throws Exception {
System.out.println(OutClass.NAME);
// Class<?> clazz = Class.forName("com.example.javademo.OutClass");
// OutClass.show();
}
}
打印結果:
20
male
class com.example.javademo.OutClass
public static void com.example.javademo.OutClass.show()
可以看到,沒有觸發(fā)類的初始化邓嘹。
執(zhí)行classInit
方法的結果:
OutClass初始化
sollian
可以看到酣栈,會先對類進行初始化。
了解了這些基礎汹押,我們來探索一個問題:內(nèi)部類的初始化是否依賴于外部類的初始化矿筝?
內(nèi)部類初始化
代碼如下:
public class OutClass {
static {
System.out.println("OutClass初始化");
}
public class InClass {
public void show() {
System.out.println("內(nèi)部類方法調(diào)用");
}
}
}
一般我們在外部獲取內(nèi)部類的實例,可以通過如下方式:
OutClass outClass = new OutClass();//先創(chuàng)建外部類實例
OutClass.InClass inClass = outClass.new InClass();//再由外部類實例創(chuàng)建內(nèi)部類實例
看下編譯后的內(nèi)部類文件OutClass$InClass.class
的構造方法
public com.example.javademo.OutClass$InClass(com.example.javademo.OutClass)
可以看到棚贾,雖然我們代碼使用了默認的無參構造函數(shù)窖维,但編譯器會自動為我們添加外部類作為第一個參數(shù)。
了解了內(nèi)部類實際的構造函數(shù)妙痹,我們可以嘗試看看是否可以通過反射的方式铸史,在不觸發(fā)外部類初始化的情況下,構建內(nèi)部類的實例怯伊。
public class Test {
public static void main(String[] args) throws Exception {
Class<OutClass.InClass> clazz = OutClass.InClass.class;
Constructor<OutClass.InClass> constructor = clazz.getConstructor(OutClass.class);
OutClass.InClass inClass = constructor.newInstance((OutClass) null);
inClass.show();
}
}
看下打印結果:
內(nèi)部類方法調(diào)用
并沒有觸發(fā)外部類的初始化琳轿!也就是說,可以在不初始化外部類的情況下耿芹,創(chuàng)建內(nèi)部類的實例對象崭篡。由此也可以間接說明,內(nèi)部類不能有靜態(tài)成員猩系,原因并不是內(nèi)部類的初始化需要依賴于外部類媚送。
內(nèi)部類不能有靜態(tài)成員的原因中燥,我目前比較傾向于是java語法的一種約束寇甸,見java的成員內(nèi)部類為什么不能有靜態(tài)變量(非final)和靜態(tài)方法呢?
如果小伙伴有其他的觀點,敬請賜教。