前言
前几天在面向 stackoverflow 编程时,遇到了一串有点诡异的代码:
1 | private String method1(byte[] bytes) { |
对java还不是很熟,乍一看还是有点懵逼的,于是就抽了个时间研究了下。
分析
这段代码其实只做了一件简单的事,就是将一个字节数组转换成一个十六进制字符串,比如说传入{1,2,126,127,-1,-2,-127,-128}
,就会输出01027e7ffffe8180
。这种类似的代码在很多需要进行编码的场景下还是很常见的。
其实正常人在写这种功能的时候是这样写的:
1 | private String method2(byte[] bytes) { |
这种代码还是比较好理解的,将一个byte转换成两个字节的十六进制字符串,通俗易懂。那method1是如何实现相同的功能的呢,这里有两个难点,理解了就简单了。
- 为什么要
& 0xff
- 为什么要
+ 0x100
并且要substring(1)
第一点,是因为java中的byte是有符号的,为了使用Integer.toString()
转换成16进制,必须要有一个从byte到int的转换。而默认的转换方法会保留符号,比如对于一个负数的byte,转换成int后符号位就提到最前面了,而我们期望的是无符号的转换。因此这里使用了& 0xff
的方式,隐式的进行了无符号的转换。
第二点,是因为在byte转换为int后,在末8位的部分有可能是以0开头,这样转换成16进制后,生成的字符串长度就会小于2,开头的0就被舍弃了。因此我们通过+ 0x100
的方式强制生成一个长度为3的字符串,再用substring(1)
将开头的1舍弃,这样就保证了输出的字符串长度一定是2。
比较
原理很简单,我感兴趣的是在 stackoverflow 上搜索的时候看到了高票答案有这样一句话:
1 | It's debatable whether all that has better performance (it certainly isn't clearer) than: |
很有趣,method1是否比method2更快竟然是有争议的,那我为啥要写这种奇怪的代码呢,速度没优势还可读性更差。从哲学上讲如果method2在任何方面都吊打method1,那么method1就没有任何存在的道理了。于是我就闲着蛋疼跑了一波微基准测试(记得在一位大佬的书里看到过这样一句话:任何在做微基准测试之前就对函数执行效率进行评论的行为都是耍流氓)。
实验
实验很简单,照着微基准测试的模板敲了(顺便mark下,以后接着用):
gradle依赖配置:
1 | compile 'org.openjdk.jmh:jmh-core:1.21' |
如果不加第二个依赖有可能会报错:
1 | Unable to find the resource: /META-INF/BenchmarkList |
测试代码:
1 | package com.pinduoduo.tusenpo.test; |
我这里测量的是函数单线程下的执行效率,比较了经过1秒钟预热以后在5秒钟内填充长度为1024的字节数组的执行次数(由于函数比较简单,这里执行时间短一点没问题)。
执行结果:
1 | # JMH version: 1.21 |
很明显,”难读”的方法比”简单”的方法还是快了几十倍的。当然,如果这一块不是性能瓶颈的话,执行一次相差零点几毫秒这点区别还是没啥意义的。