當我在Google輸入“Long類型的比較”時,會出現多如牛毛的與這個問題相關的博文,並且這些博文對此問題的看法如出一轍,都“不約而同”地持有如下觀點:
對於Long類型的數據,它是一個對象,所以對象不可以直接通過“>”,“==”,“<”的比較。若要比較是否相等,可以用Long對象的equals方法;若要進行“>”,“<”的比較,需通過Long對象的longValue方法。
那麼問題來了,這個觀點真的全對嗎?或者準確地說,後半段關於“>”,“<”的說法真的對嗎?起初我也差點信了,按理說Java中並沒有像C++中的操作符重載之類的東東,對象直接拿來用“>”或“<”比較確實很少這麼幹的,而且有童鞋可能會說,既然大家都這麼說,當然是對的無疑咯。那麼今天筆者想告訴你的是,它是錯的!Long類型可以直接用“>”和“<”比較,並且其他包裝類型也同理。不信?先別急着反駁,且聽筆者娓娓道來。
問題起源
關於Long類型的大小比較這個問題,其實是源於我的上一篇博文談談ali與Google的Java開發規範,在其中關於“相同類型的包裝類對象之間值的比較”這一規範,我補充瞭如下一點:
然後oschina上的一個熱心網友關於此提出了一個很好的問題:
即有沒有可能比較的是內存地址並且剛好其大小滿足上述條件?想想也不無道理,畢竟對於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
的當前值爲true:
此時我們通過Change Value
修改a中的value值爲100L,如圖:
然後F8到斷點2,觀察此時flagAfterAlter
的值爲false:
最後的輸出結果如下:
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(...)
從字節碼可以清晰地看到第23、27行以及第41、45行,invokevirtual,顯式調用了java/lang/Long.longValue:()
方法,確實自動拆箱了。也就是說對於基本包裝類型,除了我們之前所認知的自動裝箱和拆箱場景(關於自動裝箱和拆箱,大家可以參考這篇博文——Java中的自動裝箱與拆箱,寫的不錯,這裏我就不做過多敘述了)外,對於兩個包裝類型的>和<的操作,也會自動拆箱。無需任何testCase來佐證,結論一目瞭然。
除了Long類型,感興趣的童鞋還可以找Integer、Byte、Short等來驗證下,結果是一樣的,這裏我就不做過多敘述了。
總結
古人說得好——盡信書,則不如無書。可能,大多數的我們在面對這個問題時,都會下意識地去Google一把,然後多家博客對比查閱,最後發現幾乎所有的博文都是一致的觀點:Long對象不可直接用”>”或”<”比較,需要調用Long.longValue()
來比較。於是毫無疑問地就信了。當再次遇到這個問題時,就會“很自信”地告訴別人,要用Long.longValue()
比較。而實際呢,卻不知道自己已經陷入誤區!
雖然今天談論的只是Long對象的”>”或”<”用法問題,看起來好像是個“小問題”,最壞情況下,如果不確定是否可以直接比較,大不了直接用Long.longValue來比較,並不會阻礙你編碼。但是,筆者想說但是,作爲一個程序猿,打破砂鍋問到底的精神是不可少的,我們應該拒絕黑盒,追求細節,這樣纔可能更好地成長,在代碼的世界裏遊刃有餘。
同步更新到原文。