關(guān)于Class的幾個問題

日常工作中,我們直接接觸Class文件的時間可能不多执赡,但這不代表了解了Class文件就用處不大镰踏。本文將試圖回答三個問題,Class文件中字符串的最大長度是多少搀玖、Java存在尾遞歸調(diào)用優(yōu)化嗎余境?類的初始化順序是怎樣的灌诅?芳来。與直接給出答案不同,我們試圖從Class文件中找出這個答案背后的道理猜拾。我們一一來看一下即舌。

Class文件中字符串的最大長度是多少?

在class文件中挎袜,字符串是被存儲在常量池中顽聂,更進一步來講肥惭,它使用一種UTF-8格式的變體來存儲一個常量字符,其存儲結(jié)構(gòu)如下:

CONSTANT_Utf8_info {
    u1 tag;//值為CONSTANT_Utf8_info(1)
    u2 length;//字節(jié)的長度
    u1 bytes[length]//內(nèi)容
}

可以看到CONSTANT_Utf8_info中使用了u2類型來表示長度紊搪,當(dāng)我最開始接觸到這里的時候蜜葱,就在想一個問題,如果我聲明了一個超過u2長度(65536)的字符串耀石,是不是就無法編譯了牵囤。我們來做個實現(xiàn)。

字符串太長就不貼出來滞伟,直接貼出在終端上使用javac命令編譯后的結(jié)果:

image
image

果然揭鳞,編譯報錯了,看來class文件的確無法存儲超過65536字節(jié)的字符串梆奈。

如果事情到這里為止野崇,并沒有太大意思了,但后來我發(fā)現(xiàn)了一個有趣的事情亩钟。下面的這段代碼在eclipse中是可以編譯過的:

public class LongString {
    public static void main(String[] args){
      String s = a long long string...;
      System.out.println(s);
    }
}

這不科學(xué)乓梨,更不符合我們的認(rèn)知。eclipse搞了什么名堂径荔?我們拖出class文件看一看:

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=2, args_size=1
         0: new           #16                 // class java/lang/StringBuilder
         3: dup
         4: ldc           #18                
         6: invokespecial #20                 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
         9: ldc           #23                 // String 
        11: invokevirtual #25                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        14: invokevirtual #29                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        17: invokevirtual #33                 // Method java/lang/String.intern:()Ljava/lang/String;
        20: astore_1
        21: getstatic     #38                 // Field java/lang/System.out:Ljava/io/PrintStream;
        24: aload_1
        25: invokevirtual #44                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        28: return
      LineNumberTable:
        line 10: 0
        line 3212: 21
        line 3213: 28
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      29     0  args   [Ljava/lang/String;
           21       8     1   STR   Ljava/lang/String;

可以看到督禽,上面的超長字符串被eclipse截成兩半,#18和#23总处, 然后通過StringBuilder拼接成完整的字符串狈惫。awesome!

但是鹦马,如果我們不是在函數(shù)中聲明了一個巨長的字符串胧谈,而是在類中直接聲明:

public class LongString {

    public static final String STR = a long long string...;
  
}

Eclipse會直接進行錯誤提示:

image

具體關(guān)于在上面兩個字符串的初始化時機我們會在第三點里進行闡述,但理論上在類中直接聲明也是可以像在普通函數(shù)中一樣進行優(yōu)化荸频。具體的原因我們就不得而知了菱肖。不過這提醒我們的是在Class文件中,和字符串長度類似的還有類中繼承接口的個數(shù)旭从、方法數(shù)稳强、字段數(shù)等等,它們都是存在個數(shù)由上限的和悦。

Java存在尾遞歸調(diào)用優(yōu)化嗎退疫?

回答這個問題之前,我們需要了解什么是尾遞歸呢鸽素?借用維基百科中的回答:

  • 調(diào)用自身函數(shù)(Self-called)褒繁;
  • 計算僅占用常量棧空間(Stack Space)

用更容易理解的話來講馍忽,尾遞歸調(diào)用就是函數(shù)最后的語句是調(diào)用自身棒坏,但調(diào)用自己的時候燕差,已經(jīng)不再需要上一個函數(shù)的環(huán)境了。所以并非所有的遞歸都屬于尾遞歸坝冕,它需要通過上述的規(guī)則來編寫遞歸代碼徒探。和普通的遞歸相比,尾遞歸即使遞歸調(diào)用數(shù)萬次喂窟,它的函數(shù)棧也僅為常數(shù)刹帕,不會出現(xiàn)Stack Overflow異常。

那么java中存在尾遞歸優(yōu)化嗎谎替?這個回答現(xiàn)在是否定的,到目前的Java8為止蹋辅,Java仍然是不支持尾遞歸的钱贯。

但最近class家族的一位成員kotlin是號稱支持尾遞歸調(diào)用的,那么它是怎么實現(xiàn)的呢侦另?我們通過遞歸實現(xiàn)一個功能來對比JavaKotlin之間生成的字節(jié)碼的差別秩命。

我們來實現(xiàn)一個對兩個整數(shù)的開區(qū)間內(nèi)所有整數(shù)求和的功能。函數(shù)聲明如下:

int sum(int start, int end , int acc)

參數(shù)start為起始值褒傅,參數(shù)end為結(jié)束值弃锐,參數(shù)acc為累加值(調(diào)用時傳入0,用于遞歸使用)殿托。如sum(2,4,0)會返回9霹菊。我們分別用JavaKotlin來實現(xiàn)這個函數(shù)。

Java:

public static int sum(int start, int end , int acc){
    if(start > end){
       return acc;
    }else{
       return sum(start + 1, end, start + acc);
    }
}

Koklin:

tailrec fun sum(start: Int, end: Int, acc: Int): Int{
    if (start > end){
        return acc
    } else{
        return  sum(start+1, end, start + acc)
    }
}

我們對這兩個文件編譯生成的class文件中的sum函數(shù)進行分析:

Java生成的sum函數(shù)字節(jié)碼如下:

https://wx3.sinaimg.cn/mw690/c0755e72gy1fg25xw43qaj20s00oa412.jpg

我們提取主要信息支竹,在第14個命令上旋廷,sum函數(shù)又遞歸的調(diào)用了sum函數(shù)自己。此時礼搁,還沒有調(diào)用到第17條命令ireturn來退出函數(shù)饶碘,所以,函數(shù)棧會進行累加馒吴,如果遞歸次數(shù)過多扎运,就難免不會發(fā)生Stack Overflow異常了。

我們再來看一下Kotlin中sum函數(shù)的字節(jié)碼是怎樣的:

https://wx4.sinaimg.cn/mw690/c0755e72gy1fg25xxdnv5j20kk0vytbu.jpg

可以看到饮戳,在上面的sum函數(shù)中并沒有存在對sum自身的調(diào)用豪治,而取而代之的是,是第17條的goto命令莹捡。所以鬼吵,Kotlin尾遞歸背后的黑魔法就是將遞歸改成循環(huán)結(jié)構(gòu)。上面的代碼翻譯成我們?nèi)菀桌斫獾拇a就是如下形式:

public int sum(int start, int end , int acc){
    for(;;){
        if(start > end){
            return acc;
        }else{
            acc = start + acc;
            start = start + 1;
        }
    }    
}

通過上述的分析我們可以看到篮赢,遞歸是通過轉(zhuǎn)化為循環(huán)來降低內(nèi)存的占用齿椅。但這并不意味著寫遞歸就是很差的編程習(xí)慣琉挖。在Java這種面向?qū)ο蟮恼Z言中我們更傾向于將遞歸改成循環(huán),而在Haskell這類函數(shù)式編程語言中是將循環(huán)都改為了遞歸涣脚。在思想上并沒有優(yōu)劣之分示辈,只是解決問題的思維上的差異而已,具體表現(xiàn)就是落實到具體語言上對這兩種方法的支持程度不同而已(Java沒有尾遞歸遣蚀,Haskell沒有for矾麻、while循環(huán))。

類的初始化順序是怎樣的芭梯?

這個問題對于正在找工作的人可能比較有感覺险耀,起碼當(dāng)時我在畢業(yè)準(zhǔn)備面試題時就遇到了這個問題,并且也機械的記憶了答案玖喘。不過我們更期待的是這個答案背后的理論依據(jù)是什么甩牺。我們嘗試從class文件中找到答案。來看這樣的一段代碼:

public class InitialOrderTest {

    public static String staticField = "   StaticField";

    public String fieldFromMethod = getStrFromMethod();

    public String fieldFromInit = "   InitField";

    static {
        System.out.println( "Call Init Static Code" );
        System.out.println( staticField );
    }

    {
        System.out.println( "Call Init Block Code" );
        System.out.println( fieldFromInit );
        System.out.println( fieldFromMethod );
    }

    public InitialOrderTest()
    {
        System.out.println( "Call Constructor" );
    }

    public String getStrFromMethod(){
        System.out.println("Call getStrFromMethod Method");
        return "   MethodField" ;
    }

    public static void main( String[] args )
    {
        new InitialOrderTest();
    }
}

它運行后的結(jié)果是什么呢累奈?結(jié)果如下:

image
image

我們來一一來看一下它的class文件中的內(nèi)容贬派,首先是有一個static方法區(qū):

static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: ldc           #14                 // String    StaticField
         2: putstatic     #15                 // Field staticField:Ljava/lang/String;
         5: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
         8: ldc           #16                 // String Call Init Static Code
        10: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        13: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
        16: getstatic     #15                 // Field staticField:Ljava/lang/String;
        19: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        22: return

Java編譯器在編譯階段會將所有static的代碼塊收集到一起,形成一個特殊的方法澎媒,這個方法的名字叫做<clinit>, 這個名字容易讓我們聯(lián)想到構(gòu)造函數(shù)的名稱叫做<init>搞乏,但與構(gòu)造函數(shù)不同,這個方法在Java層中是調(diào)用不到的戒努,并且请敦,這個函數(shù)是在這個類被加載時,由虛擬機進行調(diào)用储玫。注意的是冬三,是類被加載,而不是類被初始化成實例缘缚。所以勾笆,靜態(tài)代碼塊的加載優(yōu)先于普通的代碼塊,也優(yōu)先于構(gòu)造函數(shù)桥滨。這屬于虛擬機規(guī)定的范疇窝爪,我們不做更深入的探討。

在Class文件中齐媒,是沒有為普通方法區(qū)開辟類似于<clinit>這種方法的蒲每,而是將所有普通方法區(qū)的代碼都合并到了構(gòu)造函數(shù)中,我們直接來看構(gòu)造函數(shù):

public InitialOrderTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: aload_0
         6: invokevirtual #2                  // Method getStr:()Ljava/lang/String;
         9: putfield      #3                  // Field field:Ljava/lang/String;
        12: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
        15: aload_0
        16: getfield      #3                  // Field field:Ljava/lang/String;
        19: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        22: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
        25: ldc           #6                  // String Init Block
        27: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        30: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
        33: ldc           #7                  // String Constructor
        35: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        38: return

通過分析構(gòu)造函數(shù)喻括,我們就可以對一個實例初始化的順序一清二楚邀杏,首先,0,1在構(gòu)造函數(shù)中調(diào)用了父類的構(gòu)造函數(shù)望蜡,接著唤崭,4、5脖律、6谢肾、9為成員變量進行賦值,25小泉、27在執(zhí)行實例的代碼塊芦疏,最后,33微姊、35才是執(zhí)行我們Java文件中編寫的構(gòu)造函數(shù)的代碼酸茴。這樣,一個普通類的初始化順序大致如下:

靜態(tài)代碼按照順序初始化 -> 父類構(gòu)造函數(shù) -> 變量初始化 -> 實例代碼塊 -> 自身構(gòu)造函數(shù)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末兢交,一起剝皮案震驚了整個濱河市弊决,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌魁淳,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件与倡,死亡現(xiàn)場離奇詭異界逛,居然都是意外死亡,警方通過查閱死者的電腦和手機纺座,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門息拜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人净响,你說我怎么就攤上這事少欺。” “怎么了馋贤?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵赞别,是天一觀的道長。 經(jīng)常有香客問我配乓,道長仿滔,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任犹芹,我火速辦了婚禮崎页,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘腰埂。我一直安慰自己飒焦,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布屿笼。 她就那樣靜靜地躺著牺荠,像睡著了一般翁巍。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上志电,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天曙咽,我揣著相機與錄音,去河邊找鬼挑辆。 笑死例朱,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的鱼蝉。 我是一名探鬼主播洒嗤,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼魁亦!你這毒婦竟也來了渔隶?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤洁奈,失蹤者是張志新(化名)和其女友劉穎间唉,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體利术,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡呈野,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了印叁。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片被冒。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖轮蜕,靈堂內(nèi)的尸體忽然破棺而出昨悼,到底是詐尸還是另有隱情,我是刑警寧澤跃洛,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布率触,位于F島的核電站,受9級特大地震影響汇竭,放射性物質(zhì)發(fā)生泄漏闲延。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一韩玩、第九天 我趴在偏房一處隱蔽的房頂上張望垒玲。 院中可真熱鬧,春花似錦找颓、人聲如沸合愈。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽佛析。三九已至益老,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間寸莫,已是汗流浹背捺萌。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留膘茎,地道東北人桃纯。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像披坏,于是被迫代替她去往敵國和親态坦。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容

  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法棒拂,類相關(guān)的語法伞梯,內(nèi)部類的語法,繼承相關(guān)的語法帚屉,異常的語法谜诫,線程的語...
    子非魚_t_閱讀 31,622評論 18 399
  • 第5章 引用類型(返回首頁) 本章內(nèi)容 使用對象 創(chuàng)建并操作數(shù)組 理解基本的JavaScript類型 使用基本類型...
    大學(xué)一百閱讀 3,233評論 0 4
  • //Clojure入門教程: Clojure – Functional Programming for the J...
    葡萄喃喃囈語閱讀 3,658評論 0 7
  • 看,又有一片枯黃的樹葉從樹上飄落了攻旦,這落葉就是秋天的協(xié)奏曲喻旷。春夏秋冬,四季輪回敬特,冬天帶給了我們寧靜,春天帶...
    你的社會高哥閱讀 282評論 0 0
  • 小雨下著牺陶,插著耳機漫步在操場伟阔。或許就是多愁善感的年齡和胡思亂想的天氣吧掰伸,恨不得把我剩下的大半輩子都想個地朝天...
    敏丫丫MIN閱讀 633評論 0 0