自力更生Collections.sort发现比较结果混乱?Comparator的锅还是强转类型导致?

近日开发任务时间充裕一些,于是有时间回顾一下项目。

我关注到了项目中使用的七牛云的对象存储服务。

作为测试需要上传了一些图片,但七牛的控制台却无法将内容按照上传时间排序或者是按照日期查询,由于bucket当中内容较多,我无法看到今日上传的图片记录。

点开控制台给出的帮助文档,没怎么找到控制台的说明,却在下一行中看到了这样一个小句,由愣而转笑。

又参考了segmentfault等问答,不知是自身算法实力强大还是怎样,反正七牛的确就是由一个【快】字拒绝提供一切排序和筛选服务。。

包括控制台、包括API。

因为存储使用key-value按照key的ascii排序,性能最好,展示时也决意不肯做任何变通,这是哪门子逻辑?

端详了好一会快字,又到api的文档中遨游,迫不得已结合了服务端的sdk才摸清了该使用哪个域名、以及api的验证token逻辑。

终于我可以借助api获取到一个bucket底下的所有文件列表。(当然也只能按key的字母排序)

没有排序自己排吧。

由于七牛记录的上传时间单位是100纳秒。。(醉了,下面放一张珍藏的图,可能是为了匹配内存的处理速度吧,转为毫秒要除以10000)毫无疑问会用long来表示了

先开始为了降序排序直接把compare方法直接重写为return (int)o2.putTime - o1.putTime,结果排序结果颠三倒四。

由于习惯上compare的结果都类比数学符号函数返回1/0/-1,试着修改,排序结果正常。

我慌了。印象中sort源码没有去判断1/0/-1啊。。查阅了一下,的确无论是mergeSort还是TimSort源码中只做了compare方法的结果< 0和>= 0的判断。

遂开始意识到是强转出了问题。不过,long强转为int时超过了int的范围,你们猜会怎么样?变为0?一开始我确实认为是变为0导致比较器认为很多对象相等,进而导致排序结果混乱。

实际上呢,放一些例子给大家感受一下:

原始long794056495088 强转int-512454672
原始long889116247210 强转int58016938
原始long-42421670980704 强转int-278998112
原始long-13603247092059 强转int-1085665627
原始long-42159956036917 强转int-557059381
原始long1150831190997 强转int-220044331
原始long-15560683307530 强转int-16794122
原始long-10186805000959 强转int857425153
原始long6059356417962 强转int-842436694
原始long16453936148484 强转int-83562492
原始long631537897167 强转int177704655
原始long17343052395694 强转int-25545554
原始long-9434266088448 强转int1777060864
原始long-3033381814 强转int1261585482
原始long4651744348343 强转int294766775
原始long-13649116168272 强转int289898416

出乎意料,没有0,但是有一些强转后维持了原符号,另一些正负颠倒。

是时候学习一下强转原理了。

java中long为8字节64位,int为4字节32位。

如果long强转为int:直接取低位32位作为值,但是看做补码。

在计算机中,数值以补码形式存储,正数的补码为其二进制表示。负数的补码为其模的二进制表示取反加一(或者记为符号位不变,剩余数位取反+1)。

负数的补码转原码时:符号位不变,剩余数位取反+1。和负数原码转补码的操作步骤相同哟,这里列出百度百科补码的三个特性方便理解:

1、一个负整数(或原码)与其补数(或补码)相加,和为模。
2、对一个整数的补码再求补码,等于该整数自身。
3、补码的正零与负零表示方法相同。

例1:

原始long 794056495088

原始long转为二进制 1011100011100001011101001000111111110000(40位,省略前面24个0)

取long低位32位作为int补码(结果是负数):11100001011101001000111111110000(32位,省略前面32个1)

补码转原码,符号位不变,剩余数位取反+1:10011110100010110111000000010000

转为带符号10进制:-512454672

再看一个强转后符号不改变的,这种事不多练两遍记不住。

例2:

原始long 889116247210

原始long转为二进制 1100111100000011011101010100010010101010(40位,省略前面24个0)

取低位32位作为补码(结果是正数,原码同补码):00000011011101010100010010101010

转为带符号10进制:58016938

再看一个long本身为负数的。

例3:

原始long -15560683307530

原始long转为二进制 11100010011100000001000000000100001000001010(44位,省略前面20个1)

负数先转为补码(符号位不变,剩余数位取反+1):10011101100011111110111111111011110111110110

取long低位32位作为int补码(结果是负数):11111110111111111011110111110110

补码转原码,符号位不变,剩余数位取反+1:10000001000000000100001000001010

转为带符号10进制:-16794122

所以,原始long低32位上的那个数字很重要,但是人家原来不是表示符号的(不一定会跟原始long的符号位数字相同啊),强转后就按符号位处理了,大概率要出问题。

当然,明白原理后程序上做处理就可以了。不要直接反强转结果,要么自己写下,要么使用Long.compare这个静态方法。

另外说到long,我就想到Long这些包装类,虽然本次问题与包装类无关。

写了一些测试代码,结论与大家分享:

Long l1=12345l;
Long l2=12345l;
Long l3=1234l;
System.out.println(l1>l2);
System.out.println(l1>=l2);
System.out.println(l1==l2);
System.out.println();
System.out.println(l1>l3);
System.out.println(l1>=l3);
System.out.println(l1==l3);

运行结果:

false
true
false

true
true
false

1、如果使用包装类进行等于(==)的比较,比较的是Long对象的地址,故可以使用equals或者.longvalue比较数值。

2、如果使用包装类进行包含大于小于(>、<、>=、<=)的比较,比较时jvm会进行自动拆箱,因此直接比就好了。

打破砂锅不是为了吹毛求疵,而是为了不受【常识】所限的、更好的定位问题。

最后,感谢不完善、感谢未完成,让我每日都有动力去打破砂锅、进步一点。

参考资料:

https://developer.qiniu.com/kodo/kb/1336/upload-download-instructions

https://blog.csdn.net/gdhgr/article/details/79604250

https://baike.baidu.com/item/%E8%A1%A5%E7%A0%81/6854613?fr=aladdin

原文地址:https://www.cnblogs.com/feixuefubing/p/11725143.html