類(lèi)初始化
在講類(lèi)的初始化之前励背,我們先來(lái)大概了解一下類(lèi)的聲明周期。如下圖
類(lèi)的聲明周期可以分為7個(gè)階段砸西,但今天我們只講初始化階段叶眉。我們我覺(jué)得出來(lái)使用和卸載階段外,初始化階段是最貼近我們平時(shí)學(xué)的芹枷,也是筆試做題過(guò)程中最容易遇到的衅疙,假如你想了解每一個(gè)階段的話(huà),可以看看深入理解Java虛擬機(jī)這本書(shū)鸳慈。
下面開(kāi)始講解初始化過(guò)程饱溢。
注意:
這里需要指出的是,在執(zhí)行類(lèi)的初始化之前蝶涩,其實(shí)在準(zhǔn)備階段就已經(jīng)為類(lèi)變量分配過(guò)內(nèi)存理朋,并且也已經(jīng)設(shè)置過(guò)類(lèi)變量的初始值了。例如像整數(shù)的初始值是0绿聘,對(duì)象的初始值是null之類(lèi)的嗽上。基本數(shù)據(jù)類(lèi)型的初始值如下:
數(shù)據(jù)類(lèi)型 | 初始值 | 數(shù)據(jù)類(lèi)型 | 初始值 |
---|---|---|---|
int | 0 | boolean | false |
long | 0L | float | 0.0f |
short | (short)0 | double | 0.0d |
char | '\u0000' | reference | null |
byte | (byte)0 |
大家先想一個(gè)問(wèn)題熄攘,當(dāng)我們?cè)谶\(yùn)行一個(gè)java程序時(shí)兽愤,每個(gè)類(lèi)都會(huì)被初始化嗎?假如并非每個(gè)類(lèi)都會(huì)執(zhí)行初始化過(guò)程挪圾,那什么時(shí)候一個(gè)類(lèi)會(huì)執(zhí)行初始化過(guò)程呢浅萧?
答案是并非每個(gè)類(lèi)都會(huì)執(zhí)行初始化過(guò)程,你想啊哲思,如果這個(gè)類(lèi)根本就不用用到洼畅,那初始化它干嘛,占用空間棚赔。
至于何時(shí)執(zhí)行初始化過(guò)程帝簇,虛擬機(jī)規(guī)范則是嚴(yán)格規(guī)定了有且只有5中情況會(huì)馬上對(duì)類(lèi)進(jìn)行初始化徘郭。
- 當(dāng)使用new這個(gè)關(guān)鍵字實(shí)例化對(duì)象、讀取或者設(shè)置一個(gè)類(lèi)的靜態(tài)字段丧肴,以及調(diào)用一個(gè)類(lèi)的靜態(tài)方法時(shí)會(huì)觸發(fā)類(lèi)的初始化(注意残揉,被final修飾的靜態(tài)字段除外)。
- 使用java.lang.reflect包的方法對(duì)類(lèi)進(jìn)行反射調(diào)用時(shí)芋浮,如果這個(gè)類(lèi)還沒(méi)有進(jìn)行過(guò)初始化抱环,則會(huì)觸發(fā)該類(lèi)的初始化。
- 當(dāng)初始化一個(gè)類(lèi)時(shí)纸巷,如果其父類(lèi)還沒(méi)有進(jìn)行過(guò)初始化镇草,則會(huì)先觸發(fā)其父類(lèi)。
- 當(dāng)虛擬機(jī)啟動(dòng)時(shí)何暇,用戶(hù)需要指定一個(gè)要執(zhí)行的主類(lèi)(包含main()方法的那個(gè)類(lèi))陶夜,虛擬機(jī)會(huì)先初始化這個(gè)主類(lèi)。
- 當(dāng)使用JDK 1.7的動(dòng)態(tài)語(yǔ)言支持時(shí)裆站,如果一個(gè).....(省略条辟,說(shuō)了也看不懂,哈哈)宏胯。
注意是有且只有羽嫡。這5種行為我們稱(chēng)為對(duì)一個(gè)類(lèi)的主動(dòng)引用。
初始化過(guò)程
類(lèi)的初始化過(guò)程都干了些什么呢肩袍?
在類(lèi)的初始化過(guò)程中杭棵,說(shuō)白了就是執(zhí)行了一個(gè)類(lèi)構(gòu)造器<clinit>()方法過(guò)程。注意氛赐,這個(gè)clinit并非類(lèi)的構(gòu)造函數(shù)(init())魂爪。
至于clinit()方法都包含了哪些內(nèi)容?
實(shí)際上艰管,clinit()方法是由編譯器自動(dòng)收集類(lèi)中的所有類(lèi)變量的賦值動(dòng)作和靜態(tài)語(yǔ)句塊(static{}塊)中的語(yǔ)句合并產(chǎn)生的滓侍,編譯器收集的順序則是由語(yǔ)句在源文件中出現(xiàn)的順序來(lái)決定的。并且靜態(tài)語(yǔ)句塊中只能訪問(wèn)到定義在靜態(tài)語(yǔ)句塊之前的變量牲芋,定義在它之后的變量撩笆,在前面的靜態(tài)語(yǔ)句塊可以賦值,但不能訪問(wèn)缸浦。如下面的程序夕冲。
public class Test1 {
static {
t = 10;//編譯可以正常通過(guò)
System.out.println(t);//提示illegal forward reference錯(cuò)誤
}
static int t = 0;
}
給大家拋個(gè)練習(xí)
public class Father {
public static int t1 = 10;
static {
t1 = 20;
}
}
class Son extends Father{
public static int t2 = t1;
}
//測(cè)試調(diào)用
class Test2{
public static void main(String[] args){
System.out.println(Son.t2);
}
}
輸出結(jié)果是什么呢?
答案是20裂逐。我相信大家都知道為啥歹鱼。因?yàn)闀?huì)先初始化父類(lèi)啊。
不過(guò)這里需要注意的是卜高,對(duì)于類(lèi)來(lái)說(shuō)醉冤,執(zhí)行該類(lèi)的clinit()方法時(shí)秩霍,會(huì)先執(zhí)行父類(lèi)的clinit()方法,但對(duì)于接口來(lái)說(shuō)蚁阳,執(zhí)行接口的clinit()方法并不會(huì)執(zhí)行父接口的clinit()方法。只有當(dāng)用到父類(lèi)接口中定義的變量時(shí)鸽照,才會(huì)執(zhí)行父接口的clinit()方法螺捐。
被動(dòng)引用
上面說(shuō)了類(lèi)初始化的五種情況,我們稱(chēng)之為稱(chēng)之為主動(dòng)引用矮燎。居然存在主動(dòng)定血,也意味著存在所謂的被動(dòng)引用。這里需要提出的是诞外,被動(dòng)引用并不會(huì)觸發(fā)類(lèi)的初始化澜沟。下面,我們舉例幾個(gè)被動(dòng)引用的例子:
- 通過(guò)子類(lèi)引用父類(lèi)的靜態(tài)字段峡谊,不會(huì)觸發(fā)子類(lèi)的初始化
/**
* 1.通過(guò)子類(lèi)引用父類(lèi)的靜態(tài)字段茫虽,不會(huì)觸發(fā)子類(lèi)的初始化
*/
public class FatherClass {
//靜態(tài)塊
static {
System.out.println("FatherClass init");
}
public static int value = 10;
}
class SonClass extends FatherClass {
static {
System.out.println("SonClass init");
}
}
class Test3{
public static void main(String[] args){
System.out.println(SonClass.value);
}
}
輸出結(jié)果
FatherClass init
說(shuō)明并沒(méi)有觸發(fā)子類(lèi)的初始化
- 通過(guò)數(shù)組定義來(lái)引用類(lèi),不會(huì)觸發(fā)此類(lèi)的初始化既们。
class Test3{
public static void main(String[] args){
SonClass[] sonClass = new SonClass[10];//引用上面的SonClass類(lèi)濒析。
}
}
輸出結(jié)果是啥也沒(méi)輸出。
- 引用其他類(lèi)的常量并不會(huì)觸發(fā)那個(gè)類(lèi)的初始化
public class FatherClass {
//靜態(tài)塊
static {
System.out.println("FatherClass init");
}
public static final String value = "hello";//常量
}
class Test3{
public static void main(String[] args){
System.out.println(FatherClass.value);
}
}
輸出結(jié)果:hello
實(shí)際上啥纸,之所以沒(méi)有輸出"FatherClass init",是因?yàn)樵诰幾g階段就已經(jīng)對(duì)這個(gè)常量進(jìn)行了一些優(yōu)化處理号杏,例如,由于Test3這個(gè)類(lèi)用到了這個(gè)常量"hello"斯棒,在編譯階段就已經(jīng)將"hello"這個(gè)常量?jī)?chǔ)存到了Test3類(lèi)的常量池中了盾致,以后對(duì)FatherClass.value的引用實(shí)際上都被轉(zhuǎn)化為T(mén)est3類(lèi)對(duì)自身常量池的引用了。也就是說(shuō)荣暮,在編譯成class文件之后庭惜,兩個(gè)class已經(jīng)沒(méi)啥毛關(guān)系了。
重載
對(duì)于重載渠驼,我想學(xué)過(guò)java的都懂蜈块,但是今天我們中虛擬機(jī)的角度來(lái)看看重載是怎么回事。
首先我們先來(lái)看一段代碼:
//定義幾個(gè)類(lèi)
public abstract class Animal {
}
class Dog extends Animal{
}
class Lion extends Animal{
}
class Test4{
public void run(Animal animal){
System.out.println("動(dòng)物跑啊跑");
}
public void run(Dog dog){
System.out.println("小狗跑啊跑");
}
public void run(Lion lion){
System.out.println("獅子跑啊跑");
}
//測(cè)試
public static void main(String[] args){
Animal dog = new Dog();
Animal lion = new Lion();;
Test4 test4 = new Test4();
test4.run(dog);
test4.run(lion);
}
}
運(yùn)行結(jié)果:
動(dòng)物跑啊跑
動(dòng)物跑啊跑
相信大家學(xué)過(guò)重載的都能猜到是這個(gè)結(jié)果迷扇。但是百揭,為什么會(huì)選擇這個(gè)方法進(jìn)行重載呢?虛擬機(jī)是如何選擇的呢蜓席?
在此之前我們先來(lái)了解兩個(gè)概念器一。
先來(lái)看一行代碼:
Animal dog = new Dog();
對(duì)于這一行代碼,我們把Animal稱(chēng)之為變量dog的靜態(tài)類(lèi)型厨内,而后面的Dog稱(chēng)為變量dog的實(shí)際類(lèi)型祈秕。
所謂靜態(tài)類(lèi)型也就是說(shuō)渺贤,在代碼的編譯期就可以判斷出來(lái)了,也就是說(shuō)在編譯期就可以判斷dog的靜態(tài)類(lèi)型是啥了请毛。但在編譯期無(wú)法知道變量dog的實(shí)際類(lèi)型是什么志鞍。
現(xiàn)在我們?cè)賮?lái)看看虛擬機(jī)是根據(jù)什么來(lái)重載選擇哪個(gè)方法的。
對(duì)于靜態(tài)類(lèi)型相同方仿,但實(shí)際類(lèi)型不同的變量固棚,虛擬機(jī)在重載的時(shí)候是根據(jù)參數(shù)的靜態(tài)類(lèi)型而不是實(shí)際類(lèi)型作為判斷選擇的。并且靜態(tài)類(lèi)型在編譯器就是已知的了仙蚜,這也代表在編譯階段此洲,就已經(jīng)決定好了選擇哪一個(gè)重載方法。
由于dog和lion的靜態(tài)類(lèi)型都是Animal,所以選擇了run(Animal animal)這個(gè)方法委粉。
不過(guò)需要注意的是呜师,有時(shí)候是可以有多個(gè)重載版本的,也就是說(shuō)贾节,重載版本并非是唯一的汁汗。我們不妨來(lái)看下面的代碼。
public class Test {
public static void sayHello(Object arg){
System.out.println("hello Object");
}
public static void sayHello(int arg){
System.out.println("hello int");
}
public static void sayHello(long arg){
System.out.println("hello long");
}
public static void sayHello(Character arg){
System.out.println("hello Character");
}
public static void sayHello(char arg){
System.out.println("hello char");
}
public static void sayHello(char... arg){
System.out.println("hello char...");
}
public static void sayHello(Serializable arg){
System.out.println("hello Serializable");
}
//測(cè)試
public static void main(String[] args){
char a = 'a';
sayHello('a');
}
}
運(yùn)行下代碼氮双。
相信大家都知道輸出結(jié)果是
hello char
因?yàn)閍的靜態(tài)類(lèi)型是char,隨意會(huì)匹配到sayHello(char arg);
但是碰酝,如果我們把sayHello(char arg)這個(gè)方法注釋掉,再運(yùn)行下戴差。
結(jié)果輸出:
hello int
實(shí)際上這個(gè)時(shí)候由于方法中并沒(méi)有靜態(tài)類(lèi)型為char的方法送爸,它就會(huì)自動(dòng)進(jìn)行類(lèi)型轉(zhuǎn)換∨停‘a(chǎn)'除了可以是字符袭厂,還可以代表數(shù)字97。因此會(huì)選擇int類(lèi)型的進(jìn)行重載球匕。
我們繼續(xù)注釋掉sayHello(int arg)這個(gè)方法纹磺。結(jié)果會(huì)輸出:
hello long。
這個(gè)時(shí)候'a'進(jìn)行兩次類(lèi)型轉(zhuǎn)換亮曹,即 'a' -> 97 -> 97L橄杨。所以匹配到了sayHell(long arg)方法。
實(shí)際上照卦,'a'會(huì)按照char ->int -> long -> float ->double的順序來(lái)轉(zhuǎn)換式矫。但并不會(huì)轉(zhuǎn)換成byte或者short,因?yàn)閺腸har到byte或者short的轉(zhuǎn)換是不安全的役耕。(為什么不安全采转?留給你思考下)
繼續(xù)注釋掉long類(lèi)型的方法。輸出結(jié)果是:
hello Character
這時(shí)發(fā)生了一次自動(dòng)裝箱瞬痘,'a'被封裝為Character類(lèi)型故慈。
繼續(xù)注釋掉Character類(lèi)型的方法板熊。輸出
hello Serializable
為什么?
一個(gè)字符或者數(shù)字與序列化有什么關(guān)系察绷?實(shí)際上干签,這是因?yàn)镾erializable是Character類(lèi)實(shí)現(xiàn)的一個(gè)接口,當(dāng)自動(dòng)裝箱之后發(fā)現(xiàn)找不到裝箱類(lèi)克婶,但是找到了裝箱類(lèi)實(shí)現(xiàn)了的接口類(lèi)型筒严,所以在一次發(fā)生了自動(dòng)轉(zhuǎn)型。
我們繼續(xù)注釋掉Serialiable情萤,這個(gè)時(shí)候的輸出結(jié)果是:
hello Object
這時(shí)是'a'裝箱后轉(zhuǎn)型為父類(lèi)了,如果有多個(gè)父類(lèi)摹恨,那將從繼承關(guān)系中從下往上開(kāi)始搜索筋岛,即越接近上層的優(yōu)先級(jí)越低。
繼續(xù)注釋掉Object方法晒哄,這時(shí)候輸出:
hello char...
這個(gè)時(shí)候'a'被轉(zhuǎn)換為了一個(gè)數(shù)組元素睁宰。
從上面的例子中,我們可以看出寝凌,元素的靜態(tài)類(lèi)型并非就是一定是固定的柒傻,它在編譯期根根據(jù)優(yōu)先級(jí)原則來(lái)進(jìn)行轉(zhuǎn)換。其實(shí)這也是java語(yǔ)言實(shí)現(xiàn)重載的本質(zhì)
重寫(xiě)
我們先來(lái)看一段代碼
//定義幾個(gè)類(lèi)
public abstract class Animal {
public abstract void run();
}
class Dog extends Animal{
@Override
public void run() {
System.out.println("小狗跑啊跑");
}
}
class Lion extends Animal{
@Override
public void run() {
System.out.println("獅子跑啊跑");
}
}
class Test4{
//測(cè)試
public static void main(String[] args){
Animal dog = new Dog();
Animal lion = new Lion();;
dog.run();
lion.run();
}
}
運(yùn)行結(jié)果:
小狗跑啊跑
獅子跑啊跑
我相信大家對(duì)這個(gè)結(jié)果是毫無(wú)疑問(wèn)的较木。他們的靜態(tài)類(lèi)型是一樣的红符,虛擬機(jī)是怎么知道要執(zhí)行哪個(gè)方法呢?
顯然伐债,虛擬機(jī)是根據(jù)實(shí)際類(lèi)型來(lái)執(zhí)行方法的预侯。我們來(lái)看看main()方法中的一部分字節(jié)碼
//聲明:我只是挑出了一部分關(guān)鍵的字節(jié)碼
public static void (java.lang.String[]);
Code:
Stack=2, Locals=3, Args_size=1;//可以不用管這個(gè)
//下面的是關(guān)鍵
0:new #16;//即new Dog
3: dup
4: invokespecial #18; //調(diào)用初始化方法
7: astore_1
8: new #19 ;即new Lion
11: dup
12: invokespecial #21;//調(diào)用初始化方法
15: astore_2
16: aload_1; 壓入棧頂
17: invokevirtual #22;//調(diào)用run()方法
20: aload_2 ;壓入棧頂
21: invokevirtual #22;//調(diào)用run()方法
24: return
解釋一下這段字節(jié)碼:
0-15行的作用是創(chuàng)建Dog和Lion對(duì)象的內(nèi)存空間,調(diào)用Dog,Lion類(lèi)型的實(shí)例構(gòu)造器峰锁。對(duì)應(yīng)的代碼:
Animal dog = new Dog();
Animal lion = new Lion();
接下來(lái)的16-21句是關(guān)鍵部分萎馅,16、20兩句分分別把剛剛創(chuàng)建的兩個(gè)對(duì)象的引用壓到棧頂虹蒋。17和21是run()方法的調(diào)用指令糜芳。
從指令可以看出,這兩條方法的調(diào)用指令是完全一樣的魄衅∏涂ⅲ可是最終執(zhí)行的目標(biāo)方法卻并不相同。這是為啥徐绑?
實(shí)際上:
invokevirtual方法調(diào)用指令在執(zhí)行的時(shí)候是這樣的:
- 找到棧頂?shù)牡谝粋€(gè)元素所指向的對(duì)象的實(shí)際類(lèi)型邪驮,記作C.
- 如果類(lèi)型C中找到run()這個(gè)方法,則進(jìn)行訪問(wèn)權(quán)限的檢驗(yàn)傲茄,如果可以訪問(wèn)毅访,則方法這個(gè)方法的直接引用沮榜,查找結(jié)束;如果這個(gè)方法不可以訪問(wèn)喻粹,則拋出java.lang.IllegalAccessEror異常蟆融。
- 如果在該對(duì)象中沒(méi)有找到run()方法,則按照繼承關(guān)系從下往上對(duì)C的各個(gè)父類(lèi)進(jìn)行第二步的搜索和檢驗(yàn)守呜。
- 如果都沒(méi)有找到型酥,則拋出java.lang.AbstractMethodError異常。
所以雖然指令的調(diào)用是相同的查乒,但17行調(diào)用run方法時(shí)弥喉,此時(shí)棧頂存放的對(duì)象引用是Dog,21行則是Lion玛迄。
這由境,就是java語(yǔ)言中方法重寫(xiě)的本質(zhì)。
本次的講解到此結(jié)束蓖议,希望對(duì)你有所幫助虏杰。