问题案例
先看一段业务代码,其目的是给一个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:14right.getGmtCreate()为2024-10-20 15:32:16
期望的排序结果是:先2024-10-20 15:32:16,然后2024-11-21 15:38:14,但实际恰好相反。
问题分析
- 先回答这个问题:为啥需要强转成int?
在方法最后return那一行,里面由于FakeObj.gmtCreate是Date类型,getTime()后是long类型,而这里sort其实是实现Comparator接口的compare方法(闭包语法糖):
int compare(T o1, T o2);
由于compare方法返回值是int,故需要把两个时间戳相减后的long值强转成int,否则会提示编译错误。
- 在回答关键问题:为什么时间间隔久了排序就反过来了?
一般两个时间戳相减其实也没有问题,如果给这个代码单元测试,两个时间如果如果接近时可能也发现不了问题,等发布到生产环境后,使用生产环境丰富的用户数据跑起来后就会时好时坏。而且这段代码简单的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的最大值 long和int的第一位(即符号位)为0时表示正数,为1时表示负数- 在x86架构中,
long类型的值通常存储在两个连续的32位寄存器中(例如,eax和edx),其中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;
});