首页 体育 教育 财经 社会 娱乐 军事 国内 科技 互联网 房产 国际 女人 汽车 游戏

复制粘贴一时爽:传播最广的一段Java代码曝出Bug

2020-01-08

仿制粘贴一时爽,频出 bug 火葬场。对开发者而言,Stack Overflow 和 GitHub 是最为了解不过的两大渠道,这些渠道充满着很多开源项目信息和处理各类问题的代码片段。最近,一位叫做 Aioobe 的开发者在一项查询中发现了一段自己十年前写的代码,这段代码成为了 Stack Overflow 上仿制次数最多、传达规模最广的答案,GitHub 的很多项目中也存在这段代码。但是,这位开发者表明这段代码其实是有 bug 的,并于近来更新了答案并作出阐明。

2010 年的时分,我整天泡在 Stack Overflow 上回答问题,期望能够进步自己的知名度。其时,有一个问题招引了我的留意:怎么故人类可读的格局输出字节数?举个比如,将“123456789 字节”转换为“123.5 MB”的格局输出。

这是现在的截图,但问题的确是这个

这儿的隐含范式在于所得到的字符串值应该在 1 到 999.9 之间,后边再跟上一个巨细适宜的单位。其时现已有人给了一条回应。答案中的代码以循环为根底,根本思路十分简略:测验一切单位,从最大到最小,然后运用一种显现数量小于实践字节数量的单位。用伪代码写出来,根本是这么个意思:

suffixes = [  EB ,  PB ,  TB ,  GB ,  MB ,  kB ,  B  ] 
magnitudes = [ 1018, 1015, 1012, 109, 106, 103, 100 ] 
i = 0 
while  
i++ 
printf 

一般来说,假设发布的正确答案现已获得了正分数,那后发者很难追上。在 Stack Overflow 上,这就叫“拔枪最快的赢”。不过,我认为这个答案有缺点,所以预备从头改改。我意识到,无论是 KB、MB 仍是 GB,一切单位的实质实践都是 1000 的幂,意味着应 该能够运用对数而非循环 来核算正确的量级单位。

依据以上思路,我发布了下列内容:

public static String humanReadableByteCount { 
int unit = si ? 1000 : 1024; 
if  return bytes +   B  
int exp =   / Math.log); 
String pre = .charAt + , pre); 
} 

当然,这段代码可读性不高,并且 log/pow 也或许在必定程度上影响履行功率,但至少这儿没有循环,几乎不触及分支,我觉得仍是比较整齐的。

这儿面运用的数学方法十分简略。字节计数表明为 byeCount=1000s , 其间的 s 代表小数点后的位数,求解 s,即可得出 s=log1000。

API 里没有现成的 log1000 能够直接运用,但咱们无妨用天然对数来表明,即 s = log / log。接下来,咱们取 s 的底,因为假设咱们得出的成果超越 1 MB,则期望持续运用 MB 作为表明单位。

此刻,假设 s=1,则单位为 KB;假设 s=2,则单位为 MB;依此类推,咱们将 byteCount 值除以 1000s ,然后取对应的单位。

接下来,我能做的便是等候,看看社区是否喜爱这个答案。那时分的我,必定想不到它会成为 Stack Overflow 上仿制最多的代码片段。

估量不少人看到这儿必定在想,这段代码里到底有什么 bug?再来看一遍代码:

public static String humanReadableByteCount { 
int unit = si ? 1000 : 1024; 
if  return bytes +   B  
int exp =   / Math.log); 
String pre = .charAt + , pre); 
} 

在 EB,即 1018 之后,接下来的单位应该是 ZB,即 1021。莫非是输入量过大导致“kMGTPE”字符串的索引超出规模?不是的,long 的最大值是 263 - 1 ≈ 9.2 1018,因而任何 long 值都不会超出 EB 规模。

那么,是 SI 与二进制之间存在稠浊吗?也不是。答案的前期版别中的确有这个问题,但很快就得到了修正。

那么,是不是 exp 能够为 0 会导致 charAt 发作过错?不是的。第一个 if 句子也涵盖了这种状况,因而 exp 值将一向至少为 1。

那就只剩终究一种状况了,输出成果中是否存在某些古怪的舍入过错?这正是咱们接下来要评论的部分……

这套处理方案一向运作杰出,直到字节数量到达 1 MB。假定输入为 999999 字节,那么成果将为“1000.0 kB”。虽然 999999 比 999.9 x 10001 更挨近于 1000 x 10001,但依据标准,1000 的“有用位数”超出了规模。正确的成果应该是“1.0 MB”。

无论怎么,在这个帖子的一切 22 个答案中到本文撰稿之时都存在这个过错。那么,咱们该怎么处理?

首要,咱们会留意到,一旦字节数比 999.9 x 10001更挨近于 1 x 10002,则指数就应由“k”变更为“M”。例如,9999950 就契合这种状况。相同的,当咱们输入 999950000 时,咱们应该从“M”切换为“G”,依此类推。为了达到这一方针,咱们会核算该阈值,一旦字节数超越阈值,则添加 exp:

if  * ) 
 
exp++; 

调整之后,代码即可正常作业,直到字节数挨近 1 EB。以输入为 999,949,999,999,999,999 为例,其现在的成果为 1000.0 PB,但正确成果应该是 999.9 PB。但从数学上讲,代码成果又是精确的,这又是怎么回事?这儿,咱们就遇到了 double精度机制的局限性。

浮点运算根底知识

因为选用 IEEE 754 表明方法,因而近零浮点值会十分密布,但大值则十分稀少。实践上,一切浮点值中的一半都坐落 -1 与 1 之间;而在谈到大双精度浮点数时,像 Long.MAX_VALUE 那么大的值现已没有任何含义了。

double l1 = Double.MAX_VALUE; 
 
double l2 = l1 - Long.MAX_VALUE; 
 
System.err.println; // prints true 

下面来看两项有问题的核算:

String.format 参数中的除法;

exp 进位阈值

咱们当然能够切换为 BigDecimal,但这么干就没意思了。别的,因为标准 API 中没有 BigDecimal log 函数,所以问题其实依然存在。

缩小中心值

关于第一个问题,咱们能够将字节值缩小至更合理的精度规模,一起相应调整 exp。无论怎么,终究成果都会四舍五入,因而咱们要做的便是不要放弃最低有用数字。

if  { 
 
bytes /= unit; 
 
exp--; 
 
} 

调整最低有用位

关于第二个问题,咱们当然关怀最低有用位,因而有必要得想个不同的处理方案。

首要,咱们留意到阈值存在 12 种不同的或许值,并且其间只要一种终究会发作毛病。经过以 D0016 结束这一痕迹,能够精确辨认出过错成果。一旦发作这种状况,咱们将其调整为正确值即可。

long th =   * ); 
 
if  == 0xD00 ? 52 : 0)) 
 
exp++; 

因为咱们在浮点成果中需求运用特定数位形式,因而下手的方针天然便是 strictfp,旨在保证其不受硬件运转代码的影响。

负输入

现在我还没想到什么状况下有或许需求运用负字节数量,但考虑到 Java 不支持无符号 long,咱们最好仍是把这个问题考虑进来。现在,假设输入为 -10000,那么成果为 -10000 B。这儿咱们引进 absBytes:

long absBytes = bytes == Long.MIN_VALUE ? Long.MAX_VALUE : Math.abs; 

这儿的表达之所以如此杂乱,是依据 -Long.MIN_VALUE == Long.MIN_VALUE 这一现实。现在,咱们运用 absBytes 代替 bytes 履行一切与 exp 相关的核算。

终究版别

以下是代码片段的终究版别,其间现已对开始版别做了精心调整与改善:

// 来自: https://programming.guide/the-worlds-most-copied-so-snippet.html 
public static strictfp String humanReadableByteCount { 
int unit = si ? 1000 : 1024; 
long absBytes = bytes == Long.MIN_VALUE ? Long.MAX_VALUE : Math.abs; 
if  return bytes +   B  
int exp =   / Math.log); 
long th =   * ); 
if  == 0xd00 ? 52 : 0)) exp++; 
String pre = .charAt +  { 
bytes /= unit; 
exp -= 1; 
} 
return String.format, pre); 
} 

请留意,这段代码开始的方针是防止由循环以及很多分支带来的杂乱性。在处理了一切极点状况之后,新代码的可读性要比原始版别更差。我个人是必定不会把这段代码仿制到出产代码中的。

此前,一位名叫 Sebastian Baltes 的博士生在《Empirical Software Engineering》上宣布了一篇论文,标题为《GitHub 项目中 Stack Overflow 代码片段的用法与归因》,文章讨论的中心议题只要一个:用户对代码片段的引证是否遵从 Stack Overflow 的 CC BY-SA 3.0 答应,即从 Stack Overflow 上仿制代码时,用户应保证多么程度的归因水平?

在剖析傍边,作者从 Stack Overflow 数据转储中提取出代码片段,并将其与公共 GitHub 存储库中的代码进行匹配。下面来看论文的根本发现:

咱们进行了一项大规模实证研讨,剖析了来自各公共 GitHub 项目中的十分规 Java 代码片段,对其间实践上源自 Stack Overflow 的代码片段进行了用法与归因查询。

这篇文章给出了一份表格,而其间 ID 为 3758880 的答案正是我几年前发布的那一条。到现在,这条答案获得了几十万次检查外加一千多个好评。只要在 GitHub 上随意搜搜,就能找到不计其数条 humanReadableByteCount。

这也就意味着,这段有问题的代码被很多的项目和开发者引证,要验证这段代码是否也在自己的本地存储库内,请履行以下操作:

$ git grep humanReadableByteCount 

终究,我期望告知广阔开发者 Stack Overflow 上的代码片段或许存在 bug,即便得到很多好评也改动不了这一现实;必定要对一切极点状况做出测验,特别是测验那些仿制自 Stack Overflow 的代码;浮点运算很杂乱,也很困难,在仿制代码时,请保证了解代码背面的逻辑和运用标准。

热门文章

随机推荐

推荐文章