摘要:對應(yīng)的代碼接下來的句是關(guān)鍵部分,兩句分分別把剛剛創(chuàng)建的兩個對象的引用壓到棧頂。所以雖然指令的調(diào)用是相同的,但行調(diào)用方法時,此時棧頂存放的對象引用是,行則是。這,就是語言中方法重寫的本質(zhì)。
類初始化
在講類的初始化之前,我們先來大概了解一下類的聲明周期。如下圖
類的聲明周期可以分為7個階段,但今天我們只講初始化階段。我們我覺得出來使用和卸載階段外,初始化階段是最貼近我們平時學(xué)的,也是筆試做題過程中最容易遇到的,假如你想了解每一個階段的話,可以看看深入理解Java虛擬機這本書。
下面開始講解初始化過程。
注意:
這里需要指出的是,在執(zhí)行類的初始化之前,其實在準(zhǔn)備階段就已經(jīng)為類變量分配過內(nèi)存,并且也已經(jīng)設(shè)置過類變量的初始值了。例如像整數(shù)的初始值是0,對象的初始值是null之類的。基本數(shù)據(jù)類型的初始值如下:
數(shù)據(jù)類型 | 初始值 | 數(shù)據(jù)類型 | 初始值 |
---|---|---|---|
int | 0 | boolean | false |
long | 0L | float | 0.0f |
short | (short)0 | double | 0.0d |
char | "u0000" | reference | null |
byte | (byte)0 |
大家先想一個問題,當(dāng)我們在運行一個java程序時,每個類都會被初始化嗎?假如并非每個類都會執(zhí)行初始化過程,那什么時候一個類會執(zhí)行初始化過程呢?
答案是并非每個類都會執(zhí)行初始化過程,你想啊,如果這個類根本就不用用到,那初始化它干嘛,占用空間。
至于何時執(zhí)行初始化過程,虛擬機規(guī)范則是嚴(yán)格規(guī)定了有且只有 5中情況會馬上對類進行初始化。
當(dāng)使用new這個關(guān)鍵字實例化對象、讀取或者設(shè)置一個類的靜態(tài)字段,以及調(diào)用一個類的靜態(tài)方法時會觸發(fā)類的初始化(注意,被final修飾的靜態(tài)字段除外)。
使用java.lang.reflect包的方法對類進行反射調(diào)用時,如果這個類還沒有進行過初始化,則會觸發(fā)該類的初始化。
當(dāng)初始化一個類時,如果其父類還沒有進行過初始化,則會先觸發(fā)其父類。
當(dāng)虛擬機啟動時,用戶需要指定一個要執(zhí)行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。
當(dāng)使用JDK 1.7的動態(tài)語言支持時,如果一個.....(省略,說了也看不懂,哈哈)。
注意是有且只有。這5種行為我們稱為對一個類的主動引用。
初始化過程類的初始化過程都干了些什么呢?
在類的初始化過程中,說白了就是執(zhí)行了一個類構(gòu)造器
至于clinit()方法都包含了哪些內(nèi)容?
實際上,clinit()方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態(tài)語句塊(static{}塊)中的語句合并產(chǎn)生的,編譯器收集的順序則是由語句在源文件中出現(xiàn)的順序來決定的。并且靜態(tài)語句塊中只能訪問到定義在靜態(tài)語句塊之前的變量,定義在它之后的變量,在前面的靜態(tài)語句塊可以賦值,但不能訪問。如下面的程序。
public class Test1 { static { t = 10;//編譯可以正常通過 System.out.println(t);//提示illegal forward reference錯誤 } static int t = 0; }
給大家拋個練習(xí)
public class Father { public static int t1 = 10; static { t1 = 20; } } class Son extends Father{ public static int t2 = t1; } //測試調(diào)用 class Test2{ public static void main(String[] args){ System.out.println(Son.t2); } }
輸出結(jié)果是什么呢?
答案是20。我相信大家都知道為啥。因為會先初始化父類啊。
不過這里需要注意的是,對于類來說,執(zhí)行該類的clinit()方法時,會先執(zhí)行父類的clinit()方法,但對于接口來說,執(zhí)行接口的clinit()方法并不會執(zhí)行父接口的clinit()方法。只有當(dāng)用到父類接口中定義的變量時,才會執(zhí)行父接口的clinit()方法。
被動引用上面說了類初始化的五種情況,我們稱之為稱之為主動引用。居然存在主動,也意味著存在所謂的被動引用。這里需要提出的是,被動引用并不會觸發(fā)類的初始化。下面,我們舉例幾個被動引用的例子:
1.通過子類引用父類的靜態(tài)字段,不會觸發(fā)子類的初始化
/** * 1.通過子類引用父類的靜態(tài)字段,不會觸發(fā)子類的初始化 */ 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
說明并沒有觸發(fā)子類的初始化
2.通過數(shù)組定義來引用類,不會觸發(fā)此類的初始化。
class Test3{ public static void main(String[] args){ SonClass[] sonClass = new SonClass[10];//引用上面的SonClass類。 } }
輸出結(jié)果是啥也沒輸出。
3.引用其他類的常量并不會觸發(fā)那個類的初始化
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
實際上,之所以沒有輸出"FatherClass init",是因為在編譯階段就已經(jīng)對這個常量進行了一些優(yōu)化處理,例如,由于Test3這個類用到了這個常量"hello",在編譯階段就已經(jīng)將"hello"這個常量儲存到了Test3類的常量池中了,以后對FatherClass.value的引用實際上都被轉(zhuǎn)化為Test3類對自身常量池的引用了。也就是說,在編譯成class文件之后,兩個class已經(jīng)沒啥毛關(guān)系了。
重載對于重載,我想學(xué)過java的都懂,但是今天我們中虛擬機的角度來看看重載是怎么回事。
首先我們先來看一段代碼:
//定義幾個類 public abstract class Animal { } class Dog extends Animal{ } class Lion extends Animal{ } class Test4{ public void run(Animal animal){ System.out.println("動物跑啊跑"); } public void run(Dog dog){ System.out.println("小狗跑啊跑"); } public void run(Lion lion){ System.out.println("獅子跑啊跑"); } //測試 public static void main(String[] args){ Animal dog = new Dog(); Animal lion = new Lion();; Test4 test4 = new Test4(); test4.run(dog); test4.run(lion); } }
運行結(jié)果:
動物跑啊跑
動物跑啊跑
相信大家學(xué)過重載的都能猜到是這個結(jié)果。但是,為什么會選擇這個方法進行重載呢?虛擬機是如何選擇的呢?
在此之前我們先來了解兩個概念。
先來看一行代碼:
Animal dog = new Dog();
對于這一行代碼,我們把Animal稱之為變量dog的靜態(tài)類型,而后面的Dog稱為變量dog的實際類型。
所謂靜態(tài)類型也就是說,在代碼的編譯期就可以判斷出來了,也就是說在編譯期就可以判斷dog的靜態(tài)類型是啥了。但在編譯期無法知道變量dog的實際類型是什么。
現(xiàn)在我們再來看看虛擬機是根據(jù)什么來重載選擇哪個方法的。
對于靜態(tài)類型相同,但實際類型不同的變量,虛擬機在重載的時候是根據(jù)參數(shù)的靜態(tài)類型而不是實際類型作為判斷選擇的。并且靜態(tài)類型在編譯器就是已知的了,這也代表在編譯階段,就已經(jīng)決定好了選擇哪一個重載方法。
由于dog和lion的靜態(tài)類型都是Animal,所以選擇了run(Animal animal)這個方法。
不過需要注意的是,有時候是可以有多個重載版本的,也就是說,重載版本并非是唯一的。我們不妨來看下面的代碼。
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"); } //測試 public static void main(String[] args){ char a = "a"; sayHello("a"); } }
運行下代碼。
相信大家都知道輸出結(jié)果是
hello char
因為a的靜態(tài)類型是char,隨意會匹配到sayHello(char arg);
但是,如果我們把sayHello(char arg)這個方法注釋掉,再運行下。
結(jié)果輸出:
hello int
實際上這個時候由于方法中并沒有靜態(tài)類型為char的方法,它就會自動進行類型轉(zhuǎn)換?!產(chǎn)"除了可以是字符,還可以代表數(shù)字97。因此會選擇int類型的進行重載。
我們繼續(xù)注釋掉sayHello(int arg)這個方法。結(jié)果會輸出:
hello long。
這個時候"a"進行兩次類型轉(zhuǎn)換,即 "a" -> 97 -> 97L。所以匹配到了sayHell(long arg)方法。
實際上,"a"會按照char ->int -> long -> float ->double的順序來轉(zhuǎn)換。但并不會轉(zhuǎn)換成byte或者short,因為從char到byte或者short的轉(zhuǎn)換是不安全的。(為什么不安全?留給你思考下)
繼續(xù)注釋掉long類型的方法。輸出結(jié)果是:
hello Character
這時發(fā)生了一次自動裝箱,"a"被封裝為Character類型。
繼續(xù)注釋掉Character類型的方法。輸出
hello Serializable
為什么?
一個字符或者數(shù)字與序列化有什么關(guān)系?實際上,這是因為Serializable是Character類實現(xiàn)的一個接口,當(dāng)自動裝箱之后發(fā)現(xiàn)找不到裝箱類,但是找到了裝箱類實現(xiàn)了的接口類型,所以在一次發(fā)生了自動轉(zhuǎn)型。
我們繼續(xù)注釋掉Serialiable,這個時候的輸出結(jié)果是:
hello Object
這時是"a"裝箱后轉(zhuǎn)型為父類了,如果有多個父類,那將從繼承關(guān)系中從下往上開始搜索,即越接近上層的優(yōu)先級越低。
繼續(xù)注釋掉Object方法,這時候輸出:
hello char...
這個時候"a"被轉(zhuǎn)換為了一個數(shù)組元素。
從上面的例子中,我們可以看出,元素的靜態(tài)類型并非就是一定是固定的,它在編譯期根根據(jù)優(yōu)先級原則來進行轉(zhuǎn)換。其實這也是java語言實現(xiàn)重載的本質(zhì)
重寫我們先來看一段代碼
//定義幾個類 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{ //測試 public static void main(String[] args){ Animal dog = new Dog(); Animal lion = new Lion();; dog.run(); lion.run(); } }
運行結(jié)果:
小狗跑啊跑
獅子跑啊跑
我相信大家對這個結(jié)果是毫無疑問的。他們的靜態(tài)類型是一樣的,虛擬機是怎么知道要執(zhí)行哪個方法呢?
顯然,虛擬機是根據(jù)實際類型來執(zhí)行方法的。我們來看看main()方法中的一部分字節(jié)碼
//聲明:我只是挑出了一部分關(guān)鍵的字節(jié)碼 public static void (java.lang.String[]); Code: Stack=2, Locals=3, Args_size=1;//可以不用管這個 //下面的是關(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對象的內(nèi)存空間,調(diào)用Dog,Lion類型的實例構(gòu)造器。對應(yīng)的代碼:
Animal dog = new Dog();
Animal lion = new Lion();
接下來的16-21句是關(guān)鍵部分,16、20兩句分分別把剛剛創(chuàng)建的兩個對象的引用壓到棧頂。17和21是run()方法的調(diào)用指令。
從指令可以看出,這兩條方法的調(diào)用指令是完全一樣的。可是最終執(zhí)行的目標(biāo)方法卻并不相同。這是為啥?
實際上:
invokevirtual方法調(diào)用指令在執(zhí)行的時候是這樣的:
找到棧頂?shù)牡谝粋€元素所指向的對象的實際類型,記作C.
如果類型C中找到run()這個方法,則進行訪問權(quán)限的檢驗,如果可以訪問,則方法這個方法的直接引用,查找結(jié)束;如果這個方法不可以訪問,則拋出java.lang.IllegalAccessEror異常。
如果在該對象中沒有找到run()方法,則按照繼承關(guān)系從下往上對C的各個父類進行第二步的搜索和檢驗。
如果都沒有找到,則拋出java.lang.AbstractMethodError異常。
所以雖然指令的調(diào)用是相同的,但17行調(diào)用run方法時,此時棧頂存放的對象引用是Dog,21行則是Lion。
這,就是java語言中方法重寫的本質(zhì)。
本次的講解到此結(jié)束,希望對你有所幫助。
關(guān)注公我的眾號:苦逼的碼農(nóng),獲取更多原創(chuàng)文章,后臺回復(fù)禮包送你一份特別的資源大禮包。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://www.ezyhdfw.cn/yun/76835.html
摘要:重寫語言中的定義子類方法有一個方法與父類方法的名字相同且參數(shù)類型相同。父類方法的返回值可以替換掉子類方法的返回值。思維導(dǎo)圖參考文檔極客時間深入拆解虛擬機是如何執(zhí)行方法調(diào)用的上廣告 原文 回顧Java語言中的重載與重寫,并且看看JVM是怎么處理它們的。 重載Overload 定義: 在同一個類中有多個方法,它們的名字相同,但是參數(shù)類型不同。 或者,父子類中,子類有一個方法與父類非私有方...
摘要:中,任何未處理的受檢查異常強制在子句中聲明。運行時多態(tài)是面向?qū)ο笞罹璧臇|西,要實現(xiàn)運行時多態(tài)需要方法重寫子類繼承父類并重寫父類中已 1、簡述Java程序編譯和運行的過程:答:① Java編譯程序?qū)ava源程序翻譯為JVM可執(zhí)行代碼--字節(jié)碼,創(chuàng)建完源文件之后,程序會先被編譯成 .class 文件。② 在編譯好的java程序得到.class文件后,使用命令java 運行這個 .c...
摘要:中,任何未處理的受檢查異常強制在子句中聲明。運行時多態(tài)是面向?qū)ο笞罹璧臇|西,要實現(xiàn)運行時多態(tài)需要方法重寫子類繼承父類并重寫父類中已 1、簡述Java程序編譯和運行的過程:答:① Java編譯程序?qū)ava源程序翻譯為JVM可執(zhí)行代碼--字節(jié)碼,創(chuàng)建完源文件之后,程序會先被編譯成 .class 文件。② 在編譯好的java程序得到.class文件后,使用命令java 運行這個 .c...
摘要:也就是說,一個實例變量,在的對象初始化過程中,最多可以被初始化次。當(dāng)所有必要的類都已經(jīng)裝載結(jié)束,開始執(zhí)行方法體,并用創(chuàng)建對象。對子類成員數(shù)據(jù)按照它們聲明的順序初始化,執(zhí)行子類構(gòu)造函數(shù)的其余部分。 類的拷貝和構(gòu)造 C++是默認具有拷貝語義的,對于沒有拷貝運算符和拷貝構(gòu)造函數(shù)的類,可以直接進行二進制拷貝,但是Java并不天生支持深拷貝,它的拷貝只是拷貝在堆上的地址,不同的變量引用的是堆上的...
閱讀 1745·2021-11-02 14:42
閱讀 588·2021-10-18 13:24
閱讀 1101·2021-10-12 10:12
閱讀 1890·2021-09-02 15:41
閱讀 3293·2019-08-30 15:56
閱讀 2930·2019-08-29 16:09
閱讀 2116·2019-08-29 11:13
閱讀 3690·2019-08-28 18:06