成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

從jvm角度看懂類初始化、方法重載、重寫。

tinyq / 3394人閱讀

摘要:對(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)造器()方法過程。注意,這個(gè)clinit并非類的構(gòu)造函數(shù)(init())。

至于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

相關(guān)文章

  • JVM執(zhí)行方法調(diào)用(一)- 重載重寫

    摘要:重寫語言中的定義子類方法有一個(gè)方法與父類方法的名字相同且參數(shù)類型相同。父類方法的返回值可以替換掉子類方法的返回值。思維導(dǎo)圖參考文檔極客時(shí)間深入拆解虛擬機(jī)是如何執(zhí)行方法調(diào)用的上廣告 原文 回顧Java語言中的重載與重寫,并且看看JVM是怎么處理它們的。 重載Overload 定義: 在同一個(gè)類中有多個(gè)方法,它們的名字相同,但是參數(shù)類型不同。 或者,父子類中,子類有一個(gè)方法與父類非私有方...

    韓冰 評(píng)論0 收藏0
  • 【金三銀四】面試題之java基礎(chǔ)

    摘要:中,任何未處理的受檢查異常強(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...

    Yangyang 評(píng)論0 收藏0
  • 【金三銀四】面試題之java基礎(chǔ)

    摘要:中,任何未處理的受檢查異常強(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...

    Barrior 評(píng)論0 收藏0
  • 對(duì)象

    摘要:入隊(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...

    nanfeiyan 評(píng)論0 收藏0
  • 談?wù)凧ava的面向?qū)ο?/b>

    摘要:也就是說,一個(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并不天生支持深拷貝,它的拷貝只是拷貝在堆上的地址,不同的變量引用的是堆上的...

    ormsf 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<