兩個Long類型真的不能直接用>或

  當我在Google輸入“Long類型的比較”時,會出現多如牛毛的與這個問題相關的博文,並且這些博文對此問題的看法如出一轍,都“不約而同”地持有如下觀點:

對於Long類型的數據,它是一個對象,所以對象不可以直接通過“>”,“==”,“<”的比較。若要比較是否相等,可以用Long對象的equals方法;若要進行“>”,“<”的比較,需通過Long對象的longValue方法。

那麼問題來了,這個觀點真的全對嗎?或者準確地說,後半段關於“>”,“<”的說法真的對嗎?起初我也差點信了,按理說Java中並沒有像C++中的操作符重載之類的東東,對象直接拿來用“>”或“<”比較確實很少這麼幹的,而且有童鞋可能會說,既然大家都這麼說,當然是對的無疑咯。那麼今天筆者想告訴你的是,它是錯的Long類型可以直接用“>”和“<”比較,並且其他包裝類型也同理。不信?先別急着反駁,且聽筆者娓娓道來。

問題起源

  關於Long類型的大小比較這個問題,其實是源於我的上一篇博文談談ali與Google的Java開發規範,在其中關於“相同類型的包裝類對象之間值的比較”這一規範,我補充瞭如下一點:Image1.png

  然後oschina上的一個熱心網友關於此提出了一個很好的問題:Image2.png

  即有沒有可能比較的是內存地址並且剛好其大小滿足上述條件?想想也不無道理,畢竟對於Java中的對象引用a、b、c的值實際就是對象在堆中的地址。關於這個問題,其實我最初也質疑過,爲此我編寫了多種類似上面的testCase,比如:

Long a = new Long(1000L);
Long b = new Long(2000L);
Long c = new Long(222L);
Assert.isTrue(a<b && a>c);  //斷言成功

  最終的結論跟預期一致的:兩者的比較結果跟Long對象中的數值大小的比較結果是一致的,至少從目前所嘗試過的所有testCase來看是這樣的。

從現象到本質

  但是,光靠那幾個有限的單元測試,貌似並不具有較強的說服力,心中難免總有疑惑:會不會有特殊的case沒覆蓋到?會不會還是地址比較的巧合?怎麼纔能有效地驗證我的結論呢?

  於是我開始琢磨:畢竟對於new Long()這種操作,是在堆中動態分配內存的,我們不太好控制a、b等的地址大小,那又該怎麼驗證上述的比較不是地址比較的結果呢?除了地址之外,還有別的我們能控制的嗎?有的,那就是對象中的內容!我們可以在不改變對象引用值的情況下,改變對象的內容,然後看其比較結果是否發生變化,這對於我們來說輕而易舉。有時候換個角度思考問題,就能有新的收穫!

一、debug驗證

  那麼接下來,我們就可以用反證法來證明上述問題,還是以本文開頭的testCase爲例:假設上述testCase中比較的是地址值,只要我們不對a、b進行賦值操作,即不改變它們的地址值,其比較結果就應該也是始終不變,此時我們僅修改對象中的數值,這裏對應Long對象中的value字段,使數值的大小比較與當前Long對象的比較結果相反,如果此時Long對象的比較結果也跟着變爲相反,也就推翻了地址比較這一假設,否則就是地址比較,證畢。

  接下來以實例來演示我們的推斷過程。首先上代碼:

/**
 * @author sherlockyb
 * @2018年1月14日
 */
public class JdkTest {

  @Test
  public void longCompare() {
    Long a = new Long(1000L);
    Long b = new Long(222L);

    boolean flagBeforeAlter = a > b;
    boolean flagAfterAlter = a > b; // 斷點1

    System.out.println("flagBeforeAlter: " + flagBeforeAlter
        + ", flagAfterAlter: " + flagAfterAlter); // 斷點2
  }
}

  我們以debug模式運行上述testCase,首先運行到斷點1處,此處可觀察到flagBeforeAlter的當前值爲trueImage3.png

  此時我們通過Change Value修改a中的value值爲100L,如圖:Image4.png

  然後F8到斷點2,觀察此時flagAfterAlter的值爲falseImage5.png

  最後的輸出結果如下:

flagBeforeAlter: true, flagAfterAlter: false

  由此說明,兩個Long對象直接用“>”或“<”比較時,是數值比較而非地址比較。

  好了,上面的debug測試已經能解釋我們的困惑,但是筆者認爲這還不夠!僅僅停留在表面不是我們程序猿的作風,我們要從本質——源碼出發。原理是什麼?爲什麼最終比較的是數值而不是引用?難道這也發生了自動拆箱嗎?(跟我們以前所認知的自動拆箱有出入哦)

二、迴歸本質——字節碼

  真理來自源碼。我們通過javap -c來看下剛纔那個JdkTest類,反編譯字節碼是啥:

// Compiled from "JdkTest.java"
public class org.sherlockyb.blogdemos.jdk.JdkTest {
  public org.sherlockyb.blogdemos.jdk.JdkTest();
    Code:
       0: aload_0       
       1: invokespecial #8                  // Method java/lang/Object."<init>":()V
       4: return        

  public void longCompare();
    Code:
       0: new           #17                 // class java/lang/Long
       3: dup           
       4: ldc2_w        #19                 // long 1000l
       7: invokespecial #21                 // Method java/lang/Long."<init>":(J)V
      10: astore_1      
      11: new           #17                 // class java/lang/Long
      14: dup           
      15: ldc2_w        #24                 // long 222l
      18: invokespecial #21                 // Method java/lang/Long."<init>":(J)V
      21: astore_2      
      22: aload_1       
      23: invokevirtual #26                 // Method java/lang/Long.longValue:()J
      26: aload_2       
      27: invokevirtual #26                 // Method java/lang/Long.longValue:()J
      30: lcmp          
      31: ifle          38
      34: iconst_1      
      35: goto          39
      38: iconst_0      
      39: istore_3      
      40: aload_1       
      41: invokevirtual #26                 // Method java/lang/Long.longValue:()J
      44: aload_2       
      45: invokevirtual #26                 // Method java/lang/Long.longValue:()J
      48: lcmp          
      49: ifle          56
      52: iconst_1      
      53: goto          57
      56: iconst_0      
      57: istore        4
      59: getstatic     #30                 // Field java/lang/System.out:Ljava/io/PrintStream;
      62: new           #36                 // class java/lang/StringBuilder
      65: dup           
      66: ldc           #38                 // String flagBeforeAlter: 
      68: invokespecial #40                 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
      71: iload_3       
      72: invokevirtual #43                 // Method java/lang/StringBuilder.append:(Z)Ljava/lang/StringBuilder;
      75: ldc           #47                 // String , flagAfterAlter: 
      77: invokevirtual #49                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      80: iload         4
      82: invokevirtual #43                 // Method java/lang/StringBuilder.append:(Z)Ljava/lang/StringBuilder;
      85: invokevirtual #52                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      88: invokevirtual #56                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      91: return        
}

  第59行(這裏的“行”是一種形象的描述,實指當前字節碼相對於方法體開始位置的偏移量)是我們打印結果的地方:System.out.println(...)
  從字節碼可以清晰地看到第2327行以及第4145行,invokevirtual,顯式調用了java/lang/Long.longValue:()方法,確實自動拆箱了。也就是說對於基本包裝類型,除了我們之前所認知的自動裝箱和拆箱場景(關於自動裝箱和拆箱,大家可以參考這篇博文——Java中的自動裝箱與拆箱,寫的不錯,這裏我就不做過多敘述了)外,對於兩個包裝類型的>和<的操作,也會自動拆箱。無需任何testCase來佐證,結論一目瞭然。

  除了Long類型,感興趣的童鞋還可以找Integer、Byte、Short等來驗證下,結果是一樣的,這裏我就不做過多敘述了。

總結

  古人說得好——盡信書,則不如無書。可能,大多數的我們在面對這個問題時,都會下意識地去Google一把,然後多家博客對比查閱,最後發現幾乎所有的博文都是一致的觀點:Long對象不可直接用”>”或”<”比較,需要調用Long.longValue()來比較。於是毫無疑問地就信了。當再次遇到這個問題時,就會“很自信”地告訴別人,要用Long.longValue()比較。而實際呢,卻不知道自己已經陷入誤區!

  雖然今天談論的只是Long對象的”>”或”<”用法問題,看起來好像是個“小問題”,最壞情況下,如果不確定是否可以直接比較,大不了直接用Long.longValue來比較,並不會阻礙你編碼。但是,筆者想說但是,作爲一個程序猿,打破砂鍋問到底的精神是不可少的,我們應該拒絕黑盒,追求細節,這樣纔可能更好地成長,在代碼的世界裏遊刃有餘。

同步更新到原文

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章