Java自動拆裝箱為什么不起作用了

背景

有Java基礎(chǔ)的同學(xué)都知道Java中有Primitive Type(原始類型),比如int、short。作為面向?qū)ο蟮恼Z言求晶,Java同時提供了每個原始類型的包裝類型(本質(zhì)是引用類型Reference Type),比如Integer衷笋、Long芳杏、Boolean.

為了方便大家寫代碼,JDK 5以后引入了自動拆裝箱的機制. 比如對于函數(shù):

add(Integer a)

我們在調(diào)用的時候右莱,傳一個Integer對象并不是必須的蚜锨,有時直接傳一個原始類型即可:

//int型變量直接傳
int i  = 1;
add(i);

//或者數(shù)字字面量,本身也是int型
add(5);

Java會自動將int裝換成Integer慢蜓,這個過程稱為裝箱,由于是Java自動做的郭膛,所以叫自動裝箱(autoboxing)晨抡,反之如果是將Integer自動裝換成int,則稱為自動拆箱(autounboxing)则剃。有了自動拆裝箱耘柱,平時在寫代碼的時候很Happy,瞬間覺得世界真美好~

意外

然而棍现,事情并不總是很順利调煎,比如我們有時會遇到這種場景(演示實例來自同事琛總):

//A.java有類A,A調(diào)了B的方法add(int i)己肮,這時傳的是個原始類型士袄, 完美匹配
class A {
    public static void main(String[] args) {
        B.add(1);
    }
}

//B.java
class B {   
    public static void add(int i) {
        System.out.println(i);
    }
}

執(zhí)行命令

javac A.java  //這時會同時生成A.class和B.class
java A //運行成功

然后我們做一件事,把B稍作修改谎僻,讓B的add方法接受Integer包裝類型

//B.java
class B {   
    public static void add(Integer i) { // 這里把int i 改成 Integer i后重新編譯
        System.out.println(i);
    }
}

接著重新編譯B.java文件娄柳,注意:只重新編譯B.java,相當于B類做了升級艘绍,而調(diào)用方A并不做任何改變赤拒,A.class也不重新生成。然后,我們執(zhí)行java A命令運行挎挖,結(jié)果卻并沒有像我們想象中的那樣这敬,而是報了如下錯誤

Exception in thread "main" java.lang.NoSuchMethodError: B.add(I)V
    at A.main(A.java:4)

說好的自動拆裝箱呢

NoSuchMethodError的錯誤報出來的時候,一臉的黑人問號:不是有個add(Integer i)方法嗎蕉朵?怎么會說找不到方法鹅颊?說好的自動拆裝箱呢?

說好的自動拆裝箱呢

肯定是哪里出了問題墓造!帶著問題搜到了知乎R大的一個關(guān)于Java自動拆裝箱的回答

RednaxelaFX一語道破真諦

根據(jù)R大的解釋堪伍,Java的自動拆裝箱發(fā)生在編譯期,即javac編譯的那一刻觅闽,而不是在運行期帝雇!筆者的潛意識里認為,自動拆裝箱會發(fā)生在運行期蛉拙,所以會覺得NoSuchMethodError的錯誤簡直不可思議尸闸。

如果編譯器發(fā)現(xiàn)需要自動拆裝箱,會用語法糖的方法自動給你加上Integer.valueOf()孕锄,即將A類里面的1變成Integer.valueOf(1)吮廉,然后生成在A.class文件里。但是我們編譯A文件的時候畸肆,B的add方法接受的是int型宦芦,所以A.class文件里并沒有Integer.valueOf(1)這一步,A.class文件里要調(diào)用的還是add(int i)轴脐。

后來调卑,我們把B文件的add(int i)方法變成了add(Integer i), 本質(zhì)上相當于刪除了一個舊方法,添加了一個全新的方法大咱,這個時候A.class還是老的樣子恬涧,一旦運行java A,java虛擬機就去找B中的add(int i)方法碴巾,然而它已經(jīng)找不到這個方法了溯捆,因為已經(jīng)被刪除了,只留下了add(Integer i)方法厦瓢,所以會報NoSuchMethodError.

繼續(xù)扒開自動拆裝箱的底褲

我們執(zhí)行以下命令來查看A.class的具體信息

javap -verbose A.class

當B類的方法為add(int i)時提揍,A.class的信息如下:

class A
  minor version: 0
  major version: 52
  flags: ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#13         // java/lang/Object."<init>":()V
   #2 = Methodref          #14.#15        // B.add:(I)V
   #3 = Class              #16            // A
   #4 = Class              #17            // java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Utf8               Code
   #8 = Utf8               LineNumberTable
   #9 = Utf8               main
  #10 = Utf8               ([Ljava/lang/String;)V
  #11 = Utf8               SourceFile
  #12 = Utf8               A.java
  #13 = NameAndType        #5:#6          // "<init>":()V
  #14 = Class              #18            // B
  #15 = NameAndType        #19:#20        // add:(I)V
  #16 = Utf8               A
  #17 = Utf8               java/lang/Object
  #18 = Utf8               B
  #19 = Utf8               add
  #20 = Utf8               (I)V
{
  A();
    descriptor: ()V
    flags:
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=1, args_size=1
         0: iconst_1
         1: invokestatic  #2                  // Method B.add:(I)V
         4: return
      LineNumberTable:
        line 4: 0
        line 5: 4
}
SourceFile: "A.java"

重點關(guān)注下1: invokestatic #2這段,即對應(yīng)A.java中的add(1)要調(diào)用add方法的邏輯旷痕,#2指向常量池#2 = Methodref #14.#15, 然后把 #14.#15繼續(xù)展開碳锈,即 #18.#19:#20 , 最后展開的樣子其實就是注釋的樣子 B.add:(I)V,這說明到了匯編這一層欺抗,運行期找的就是add(int i)方法售碳。

然后,如果我們把B類的方法改為add(Integer i)時,重新編譯后的A.class的信息如下:

class A
  minor version: 0
  major version: 52
  flags: ACC_SUPER
Constant pool:
   #1 = Methodref          #5.#14         // java/lang/Object."<init>":()V
   #2 = Methodref          #15.#16        // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
   #3 = Methodref          #17.#18        // B.add:(Ljava/lang/Integer;)V
   #4 = Class              #19            // A
   #5 = Class              #20            // java/lang/Object
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               main
  #11 = Utf8               ([Ljava/lang/String;)V
  #12 = Utf8               SourceFile
  #13 = Utf8               A.java
  #14 = NameAndType        #6:#7          // "<init>":()V
  #15 = Class              #21            // java/lang/Integer
  #16 = NameAndType        #22:#23        // valueOf:(I)Ljava/lang/Integer;
  #17 = Class              #24            // B
  #18 = NameAndType        #25:#26        // add:(Ljava/lang/Integer;)V
  #19 = Utf8               A
  #20 = Utf8               java/lang/Object
  #21 = Utf8               java/lang/Integer
  #22 = Utf8               valueOf
  #23 = Utf8               (I)Ljava/lang/Integer;
  #24 = Utf8               B
  #25 = Utf8               add
  #26 = Utf8               (Ljava/lang/Integer;)V
{
  A();
    descriptor: ()V
    flags:
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=1, args_size=1
         0: iconst_1
         1: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
         4: invokestatic  #3                  // Method B.add:(Ljava/lang/Integer;)V
         7: return
      LineNumberTable:
        line 4: 0
        line 5: 7
}
SourceFile: "A.java"

重點在這里

     0: iconst_1
     1: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
     4: invokestatic  #3                  // Method B.add:(Ljava/lang/Integer;)V
     7: return

明顯多了一行invokestatic #2, 而這一行顯然就是Integer.valueOf(1)的過程贸人,即自動裝箱的過程间景,也就是編譯器自動幫我們加上的一段代碼,之后4: invokestatic #3才去調(diào)用B類的add(Integer i) 方法艺智。

所以倘要,我們潛意識里以為會在運行期執(zhí)行的拆裝箱的過程,其實在編譯期就做好了十拣;在運行期JVM會嚴格按照class文件中的執(zhí)行過程來尋找相應(yīng)的匹配方法封拧,而add(int i)和add(Integer i)方法顯然不是同一個方法,當然會報NoSuchMethodError.

聊聊代碼兼容問題

以上案例是我們刻意設(shè)計出來的夭问,其實在真實的場景中是會碰到這種問題的泽西。比如我們在自己的項目中引用了兩個不同項目jar1 和jar2,而jar1同時引用了jar2的方法缰趋,如果jar2把方法add(int i)改為add(Integer i)捧杉,而我們引用的jar1還是老的,這個時候我們項目再去調(diào)jar1里的方法秘血,jar1再調(diào)新jar2的方法add(int)味抖,就會報NoSuchMethodError.

其實這類問題背后反映的是代碼兼容性的問題,比如:

//B類version1
class B {
    public static void add(int i) {
        System.out.println(i);
    }
}

//B類version2
class B {
    public static void add(Integer i) {
        System.out.println(i);
    }
}

B類從version1 到 version2的升級灰粮,并不是一個兼容性的升級仔涩, add(int i)方法和 add(Integer i)不是同一個方法,version2的版本相當于刪除了原來的方法谋竖,新加了一個方法红柱,如果有歷史jar包還在調(diào)用老的方法而且沒有重新編譯,而且JVM中加載的又是version2的B類蓖乘,那么最終的結(jié)果一定是報錯。

兼容性的升級是重載這個方法:

//B類version3
class B {
    public static void add(int i) {
        System.out.println(i);
    }
    public static void add(Integer i) {
        System.out.println(i);
    }
}

類似的代碼兼容性問題還有很多韧骗,比如我們給別人提供的RPC方法中嘉抒,顯然是不能隨便刪除字段的,這個很容易理解袍暴,刪除字段后些侍,別人在線上跑的應(yīng)用用的還是舊的API,他們獲取不到想要的字段肯定是會出問題的政模。

而添加字段就是一個對兼容友好的升級行為岗宣,我們添加的字段,使用舊API的消費方雖然看不到新字段的存在淋样,但是老字段依然還是可用的耗式。比如服務(wù)端給客戶端提供的JSON API,客戶端只關(guān)心自己需要的字段,服務(wù)端添加字段上線刊咳,并不會影響老版本的客戶端的使用彪见,因為老版本客戶端在做JSON反序列化的時候只根據(jù)字段名反序列化。

而服務(wù)端通信協(xié)議Thrift的反序列化和JSON又不一樣娱挨,Thrift反序列化過程并不以是字段名為參考余指,而是和順序強相關(guān),比如對于Thrfit 的 struct類型:

//Person version1
struct Person{
 1:i32 id
 2:string name
}

//Person version2
struct Person{
 1:i32 id
 2:i32 age
 3:string name
}

//Person version3
struct Person{
 1:i32 id
 2:string name
 3:i32 age
}

如果服務(wù)提供方將Person升級到version2跷坝,那么對于還在使用version1 的消費者來說酵镜,Person實例請求回來要反序列化的時候,會把第二個age反序列化成name柴钻,顯然這不是我們想要的結(jié)果淮韭,而對兼容友好的升級應(yīng)該是version3那種,不影響前面字段的排序顿颅,在后面添加字段缸濒,這樣就不會對老版本的API造成反序列的錯亂。

軟件行業(yè)兼容性的典型案例就是微軟家的Windows操作系統(tǒng)粱腻,有人嘗試過過把Windows 3.1的很多程序放到Windows XP上去安裝使用庇配,竟然發(fā)現(xiàn)還能正常運行,甚至很流暢绍些,真是驚嘆Windows對兼容性的執(zhí)著捞慌!有人說,Windows幾乎是業(yè)界兼容性做的最好的OS柬批,而這一點也許是Windows在桌面市場領(lǐng)域能獨占鰲頭的重要原因之一啸澡。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市氮帐,隨后出現(xiàn)的幾起案子嗅虏,更是在濱河造成了極大的恐慌,老刑警劉巖上沐,帶你破解...
    沈念sama閱讀 211,884評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件皮服,死亡現(xiàn)場離奇詭異,居然都是意外死亡参咙,警方通過查閱死者的電腦和手機龄广,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,347評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蕴侧,“玉大人择同,你說我怎么就攤上這事【幌” “怎么了敲才?”我有些...
    開封第一講書人閱讀 157,435評論 0 348
  • 文/不壞的土叔 我叫張陵裹纳,是天一觀的道長。 經(jīng)常有香客問我归斤,道長,這世上最難降的妖魔是什么脏里? 我笑而不...
    開封第一講書人閱讀 56,509評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮番舆,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘矾踱。我一直安慰自己恨狈,他們只是感情好呛讲,可當我...
    茶點故事閱讀 65,611評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著贝搁,像睡著了一般吗氏。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上雷逆,一...
    開封第一講書人閱讀 49,837評論 1 290
  • 那天弦讽,我揣著相機與錄音膀哲,去河邊找鬼。 笑死某宪,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的兴喂。 我是一名探鬼主播,決...
    沈念sama閱讀 38,987評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼娩嚼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起佃迄,我...
    開封第一講書人閱讀 37,730評論 0 267
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎呵俏,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體吼肥,經(jīng)...
    沈念sama閱讀 44,194評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡麻车,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,525評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了啤斗。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片赁咙。...
    茶點故事閱讀 38,664評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖彼水,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情猿涨,我是刑警寧澤,帶...
    沈念sama閱讀 34,334評論 4 330
  • 正文 年R本政府宣布澡绩,位于F島的核電站俺附,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏事镣。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,944評論 3 313
  • 文/蒙蒙 一氛琢、第九天 我趴在偏房一處隱蔽的房頂上張望随闪。 院中可真熱鬧,春花似錦铐伴、人聲如沸俏讹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,764評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽玲献。三九已至,卻和暖如春株依,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背恋腕。 一陣腳步聲響...
    開封第一講書人閱讀 31,997評論 1 266
  • 我被黑心中介騙來泰國打工逆瑞, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人获高。 一個月前我還...
    沈念sama閱讀 46,389評論 2 360
  • 正文 我出身青樓念秧,卻偏偏與公主長得像,于是被迫代替她去往敵國和親摊趾。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,554評論 2 349

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