两个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的当前值为true

Image3.png

此时我们通过Change Value修改a中的value值为100L,如图:

Image4.png

然后F8到断点2,观察此时flagAfterAlter的值为false

Image5.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来比较,并不会阻碍你编码。但是,笔者想说但是,作为一个程序猿,打破砂锅问到底的精神是不可少的,我们应该拒绝黑盒,追求细节,这样才可能更好地成长,在代码的世界里游刃有余。

同步更新到原文

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,923评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,154评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,775评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,960评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,976评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,972评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,893评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,709评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,159评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,400评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,552评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,265评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,876评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,528评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,701评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,552评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,451评论 2 352

推荐阅读更多精彩内容

  • 浓黑的头发,颚骨微微突出,一双深邃清亮的眼因经常熬夜有了血丝而略显暗淡,颧骨上的胡渣看起来很硬,像五月刚割过...
    我罪珍贵阅读 504评论 0 0