一段排序代码踩到int转long溢出致正数变负数的坑

38 阅读4分钟

问题案例

先看一段业务代码,其目的是给一个fakeObjList按其gmtCreate字段从小到大排序

// 按FakeObj创建时间gmtCreate字段小到大排序
Collections.sort(fakeObjList, (left, right) -> {
    if (left == right) {
        return 0;
    }
    if (left == null) {
        return -1;
    }
    if (right == null) {
        return 1;
    }
    // (小提示) 左减右 就是从小到大排序,反过来 就是从大到小排序
    return (int) (left.getGmtCreate().getTime() - right.getGmtCreate().getTime());
});

问题重现

当两个时间比较接近时,排序是正常的,但当两个时间相隔比较久时就排序反过来了,比如这个例子

  • left.getGmtCreate()2024-11-21 15:38:14
  • right.getGmtCreate()2024-10-20 15:32:16

期望的排序结果是:先2024-10-20 15:32:16,然后2024-11-21 15:38:14,但实际恰好相反。

问题分析

  1. 先回答这个问题:为啥需要强转成int?

在方法最后return那一行,里面由于FakeObj.gmtCreateDate类型,getTime()后是long类型,而这里sort其实是实现Comparator接口的compare方法(闭包语法糖):

int compare(T o1, T o2);

由于compare方法返回值是int,故需要把两个时间戳相减后的long值强转成int,否则会提示编译错误。

  1. 在回答关键问题:为什么时间间隔久了排序就反过来了?

一般两个时间戳相减其实也没有问题,如果给这个代码单元测试,两个时间如果如果接近时可能也发现不了问题,等发布到生产环境后,使用生产环境丰富的用户数据跑起来后就会时好时坏。而且这段代码简单的debug还发现不容易看出问题,只能是分别打印下面的几个值:

left.getGmtCreate().getTime() 
// 值为 1732174694000  (即2024-11-21 15:38:14)

right.getGmtCreate().getTime() 
// 值为 1729409536000  (即2024-10-20 15:32:16)

left.getGmtCreate().getTime() - right.getGmtCreate().getTime() 
// 值为2765158000(正数)

(int) (left.getGmtCreate().getTime() - right.getGmtCreate().getTime()) 
// 值为-1529809296(负数)

(int)2765158000L 
// 值为-1529809296(负数)

其实问题的核心就是long转int时溢出了导致正数变负数。

原理解析

先了解下几个基础知识:

  • java中int是32位,最大值是:  =2147483647,用16进制表示是0x7fffffff,用二进制是0111 1111 1111 1111 1111 1111 1111 1111
  • java中long是64位,两个long的差值很容易大于int的最大值
  • longint的第一位(即符号位)为0时表示正数,为1时表示负数
  • x86架构中,long类型的值通常存储在两个连续的32位寄存器中(例如,eaxedx),其中eax存储低32位,而edx存储高32位。
  • java中强转int对应的jvm指令是l2i,可通过javap -verbose xxx.class,然后搜索l2i来直接查看字节码源码
  • jvm的l2i指令实际上会忽略long值的高32位,只保留低32位。在汇编层面,这通常意味着将eax寄存器的值直接移动到另一个寄存器或内存位置去使用即可。

那么如果long的第一位(符号位)和第32位的二进制不相同时,数字正负就会正好反过来。

回到本案例中以二进制来看这个案例更好理解(可借用10进制转2进制和2进制转10进制工具查看):

两个时间戳差值为2765158000,该long的64位二进制为:
0000000000000000000000000000000010100100110100001111011001110000
其首位为0,表示正数

把上面的long转int时,如果丢弃的二进制数字替换为下划线(方便对齐),那强转int后的32位二进制为:
________________________________10100100110100001111011001110000
可见首位为1,其值为-1529809296,为负数

解决方案

其实排序时用到的Comparator接口返回的int不需要是精确的时间戳差值,只需要大小即可,可以直接用1表示大,-1表示小,0表示相等。修改后如下:

// 按FakeObj创建时间gmtCreate字段小到大排序
Collections.sort(fakeObjList, (left, right) -> {
    if (left == right) {
        return 0;
    }
    if (left == null) {
        return -1;
    }
    if (right == null) {
        return 1;
    }
    // 修改后的效果:
    return left.getGmtCreate().getTime() - right.getGmtCreate().getTime() > 0L ? 1 : -1;
});