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