類是在任何static成員被訪問時(shí)加載的(構(gòu)造器也是static方法)秕铛。類的整個(gè)加載過程包括加載、驗(yàn)證缩挑、準(zhǔn)備但两、解析、初始化5個(gè)階段供置。我這里只討論我們?cè)诠P試題中比較關(guān)心的谨湘、影響程序輸出的部分。
類加載:
在準(zhǔn)備階段芥丧,static變量在方法區(qū)被分配內(nèi)存紧阔,然后內(nèi)存被初始化零值(注意和static變量初始化的區(qū)別)。
在初始化階段续担,執(zhí)行類構(gòu)造器<clinit>()方法(注意和實(shí)例構(gòu)造器<init>()方法不同)擅耽。虛擬機(jī)會(huì)保證子類的<clinit>()方法執(zhí)行之前,父類的<clinit>()方法已經(jīng)執(zhí)行物遇。
在執(zhí)行<clinit>()方法時(shí)乖仇,按照類定義中static變量的賦值語句和static代碼段的書寫順序憾儒,依次執(zhí)行。
子類調(diào)用基類的靜態(tài)方法時(shí)这敬,相當(dāng)于基類調(diào)用自己的靜態(tài)方法航夺,所以子類的static不會(huì)初始化。例子如下:
Child.sMethodBase(); // 類的定義在最后面
這一句的執(zhí)行結(jié)果為:
基類initPrint2 靜態(tài)變量s4:null
基類靜態(tài)方法sMethodBase 靜態(tài)變量s4:基類靜態(tài)變量s4
創(chuàng)建對(duì)象:
虛擬機(jī)在遇到new指令時(shí)崔涂,首先檢查類是否加載過,在類加載檢查通過后始衅,虛擬機(jī)為對(duì)象分配內(nèi)存冷蚂,分配完內(nèi)存后會(huì)將內(nèi)存空間初始化為零值(不包括對(duì)象頭)。所以對(duì)象的實(shí)例字段在初始化之前就有了零值汛闸。
執(zhí)行new指令之后會(huì)接著執(zhí)行實(shí)例構(gòu)造器<init>方法蝙茶,這時(shí)才開始對(duì)象的初始化。
進(jìn)入構(gòu)造器時(shí)诸老,如果有基類隆夯,會(huì)進(jìn)入基類的無參構(gòu)造器(或者用super()顯式指定的基類構(gòu)造器)。在構(gòu)造之前别伏,先按照實(shí)例字段和非static代碼段的書寫順序蹄衷,依次初始化,最后執(zhí)行構(gòu)造器的語句厘肮。
super()語句要按基類的次序愧口,放在構(gòu)造器最前面,否則編譯器會(huì)報(bào)錯(cuò)类茂。
創(chuàng)建子類對(duì)象的例子如下:
Child child = new Child("s");
輸出結(jié)果為:
基類initPrint2 靜態(tài)變量s4:null
子類initPrint2 靜態(tài)變量s2:null
基類initPrint1 實(shí)例變量s3:null
基類initPrint1 靜態(tài)變量s4:基類靜態(tài)變量s4
基類構(gòu)造器 int i
子類initPrint1 實(shí)例變量s1:null
子類initPrint1 靜態(tài)變量s2:子類靜態(tài)變量s2
子類構(gòu)造器
可見耍属,確實(shí)是先加載類(第1、2行發(fā)生在static變量的初始化階段)巩检,然后再創(chuàng)建對(duì)象(第3行及以后)厚骗。創(chuàng)建的過程也是從父類到子類,先是非static變量的初始化(初始化前已經(jīng)有默認(rèn)值了兢哭,如第3行和第6行所示)领舰,然后執(zhí)行構(gòu)造器語句。
上面用到的類的定義如下:
class Base {
private int x3 = initPrint1();
public String s3 = "基類實(shí)例變量s3";
private static int x4 = initPrint2();
private static String s4 = "基類靜態(tài)變量s4";
private int initPrint1() {
System.out.println("基類initPrint1 實(shí)例變量s3:" + s3);
System.out.println("基類initPrint1 靜態(tài)變量s4:" + s4);
return 11;
}
private static int initPrint2() {
System.out.println("基類initPrint2 靜態(tài)變量s4:" + s4);
return 21;
}
public Base(int i) {
System.out.println("基類構(gòu)造器 int i");
}
public void callName() {
System.out.println(s3);
}
public static void sMethodBase() {
System.out.println("基類靜態(tài)方法sMethodBase 靜態(tài)變量s4:"+s4);
}
}
class Child extends Base {
private int x1 = initPrint1();
public String s1 = "子類實(shí)例變量s1";
private static int x2 = initPrint2();
private static String s2 = "子類靜態(tài)變量s2";
private int initPrint1() {
System.out.println("子類initPrint1 實(shí)例變量s1:" + s1);
System.out.println("子類initPrint1 靜態(tài)變量s2:" + s2);
return 11;
}
private static int initPrint2() {
System.out.println("子類initPrint2 靜態(tài)變量s2:" + s2);
return 21;
}
public Child(String s) {
super(1);
System.out.println("子類構(gòu)造器");
}
public void callName() {
System.out.println(s1);
}
public static void sMethodChild() {
System.out.println("子類靜態(tài)方法sMethodChild 靜態(tài)變量s2:"+s2);
}
}
方法和字段的重寫
另一個(gè)基礎(chǔ)的問題是子類對(duì)父類的override厦瓢。
方法的重寫有運(yùn)行時(shí)綁定的效果提揍,子類實(shí)例如果重寫了基類的方法,即使向上轉(zhuǎn)型為基類煮仇,調(diào)用的仍是子類的方法劳跃。而且在方法中的字段也會(huì)優(yōu)先認(rèn)為是子類的字段。
但是字段并沒有運(yùn)行時(shí)綁定一說浙垫,向上轉(zhuǎn)型后調(diào)用的就是基類的字段刨仑。
同時(shí)靜態(tài)方法與類關(guān)聯(lián)郑诺,并不是與單個(gè)對(duì)象關(guān)聯(lián),它也沒有運(yùn)行時(shí)綁定杉武。
class Base {
public String s1 = "基類實(shí)例變量s1";
private static String s2 = "基類靜態(tài)變量s2";
public void f() {
System.out.println("基類方法");
}
}
class Child extends Base {
public String s1 = "子類實(shí)例變量s1";
private static String s2 = "子類靜態(tài)變量s2";
public void f() {
System.out.println("子類方法");
}
}
對(duì)于上面的兩個(gè)類辙诞,當(dāng)如下使用時(shí):
Child child = new Child();
System.out.println(((Base)child).s1);
((Base)child).f();
輸出的結(jié)果為:
基類實(shí)例變量s1
子類方法
需要補(bǔ)充說明的是,private的方法雖然可以重寫轻抱,但已經(jīng)不是傳統(tǒng)意義上的override飞涂,因?yàn)楦割惖膒rivate方法對(duì)子類不可見,所以子類重寫的函數(shù)被認(rèn)為是新函數(shù)祈搜,在父類函數(shù)中將子類向上轉(zhuǎn)型時(shí)较店,調(diào)用的仍是父類的private方法,這是在類加載的解析階段就確定的容燕。
class Base {
public String s1 = "基類實(shí)例變量s1";
private void f() {
System.out.println("基類方法");
}
public static void main(String[] args) {
Child child = new Child();
System.out.println(((Base)child).s1);
((Base)child).f();
}
}
class Child extends Base {
public String s1 = "子類實(shí)例變量s1";
public void f() {
System.out.println("子類方法");
}
}
Base的main函數(shù)運(yùn)行結(jié)果為:
基類實(shí)例變量s1
基類方法
解析階段中確定唯一調(diào)用版本的方法有static方法梁呈、private方法、實(shí)例構(gòu)造器和父類方法4類蘸秘,滿足“編譯器可知官卡,運(yùn)行期不變”的要求。
綜合題
最后我們來看一道糯茁玻客網(wǎng)上的題目:
public class Base
{
private String baseName = "base";
public Base()
{
callName();
}
public void callName()
{
System. out. println(baseName);
}
static class Sub extends Base
{
private String baseName = "sub";
public void callName()
{
System. out. println (baseName) ;
}
}
public static void main(String[] args)
{
Base b = new Sub();
}
}
程序的輸出結(jié)果是什么呢寻咒?
5
4
3
2
1
null (子類重寫了父類的public方法,public實(shí)例方法屬于運(yùn)行時(shí)綁定的方法灰粮,實(shí)際調(diào)用時(shí)仔涩,傳入的this引用的是一個(gè)子類對(duì)象,所以定位到了子類的函數(shù)粘舟,而父類構(gòu)造時(shí)熔脂,子類還未構(gòu)造,子類實(shí)例變量還沒有初始化柑肴,為零值)霞揉。如果將Base里面的public callName()修改為private callName()結(jié)果會(huì)是什么呢?
5
4
3
2
1
base(子類并沒有真正重寫父類的callName()方法晰骑,它們是兩個(gè)不同的方法适秩,private方法在類加載階段解析,滿足“編譯器可知硕舆,運(yùn)行期不變”的要求秽荞,和調(diào)用者的實(shí)際類型無關(guān),根據(jù)上下文信息抚官,父類構(gòu)造器中的方法被指定為父類中的私有方法)扬跋。