关于java:Stack-Overflow-最火的一段代码竟然有-Bug

43次阅读

共计 5032 个字符,预计需要花费 13 分钟才能阅读完成。

某天,我忙中偷闲去 Stack Overflow 上赚声望值。

于是,我看到了上面这个问题:怎么将字节数输入成人类可读的格局?也就是说,怎么将 123,456,789 字节输入成 123.5MB?

隐含的条件是,后果字符串该当在 1~999.9 的范畴内,前面跟一个适当的示意单位的后缀。

这个问题曾经有一个答案了,代码是用循环写的。基本思路很简略:尝试所有尺度,从最大的 EB(10^18 字节)开始直到最小的 B(1 字节),而后抉择小于字节数的第一个尺度。用伪代码来示意的话大抵如下:

suffixes   = ["EB", "PB", "TB", "GB", "MB", "kB", "B"]
magnitudes = [1018, 1015, 1012, 109, 106, 103, 100]
i = 0
while (i < magnitudes.length && magnitudes[i] > byteCount)
i++
printf("%.1f %s", byteCount / magnitudes[i], suffixes[i])

通常,如果一个问题曾经有了正确答案,并且有人赞过,别的答复就很难赶超了。不过这个答案有一些问题,所以我仍然有机会超过它。至多,循环还有很大的清理空间。

1、这只是一个代数问题!

而后我就想到,kB、MB、GB……等后缀只不过是 1000 的幂(或者在 IEC 规范下是 1024 的幂),也就是说不须要应用循环,齐全能够应用对数来计算正确的后缀。

依据这个想法,我写出了上面的答案:

public static String humanReadableByteCount(long bytes, boolean si) {
    int unit = si ? 1000 : 1024;
    if (bytes < unit) return bytes + "B";
    int exp = (int) (Math.log(bytes) / Math.log(unit));
    String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp-1) + (si ? "":"i");
    return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre);
}

当然,这段代码并不是太好了解,而且 log 和 pow 的组合的效率也不高。但我没有应用循环,而且没有任何分支,看起来很洁净。

这段代码的数学原理很简略。字节数示意为 byteCount = 1000^s,其中 s 示意尺度。(对于二进制记法令应用 1024 为底。)求解 s 可得 s = log_1000(byteCount)。

API 并没有提供 log_1000,但咱们能够用自然对数示意为 s = log(byteCount) / log(1000)。而后对 s 向下取整(强制转换为 int),这样对于大于 1MB 但有余 1GB 的都能够用 MB 来示意。

此时如果 s =1,尺度就是 kB,如果 s =2,尺度就是 MB,以此类推。而后将 byteCount 除以 1000^s,并找出正确的后缀。

接下来,我就等着社区的反馈了。我并不知道这段代码起初成了被复制粘贴最多的代码。

2、对于奉献的钻研

到了 2018 年,一位名叫 Sebastian Baltes 的博士生在《Empirical Software Engineering》杂志上发表了一篇论文,题为《Usage and Attribution of Stack Overflow Code Snippets in GitHub Projects》。

该论文的宗旨能够概括成一点:人们是否在恪守 Stack Overflow 的 CC BY-SA 3.0 受权?也就是说,当人们从 Stack Overflow 上复制粘贴时,会怎么注明起源?

作为剖析的一部分,他们从 Stack Overflow 的数据转出中提取了代码片段,并与公开的 GitHub 代码库中的代码进行匹配。论文摘要如是说:

We present results of a large-scale empirical study analyzing the usage and attribution of non-trivial Java code snippets from SO answers in public GitHub (GH) projects.

(本文对于在公开的 GitHub 我的项目中应用来自 Stack Overflow 上有价值的代码片段的状况以及起源注明状况进行了大规模的教训剖析,并给出了后果。)(剧透:绝大多数人并不会注明起源。)

论文中有这样一张表格:

id 为 3758880 的答案正是我八年前贴出的答案。此时该答案曾经被浏览了几十万次,领有上千个赞。

在 GitHub 上轻易搜寻一下就能找到数千个 humanReadableByteCount 函数:

你能够用上面的命令看看本人有没有无心中用到:

$ git grep humanReadableByteCount

3、问题

你必定在想:这段代码有什么问题:

再来看一次:

public static String humanReadableByteCount(long bytes, boolean si) {
    int unit = si ? 1000 : 1024;
    if (bytes < unit) return bytes + "B";
    int exp = (int) (Math.log(bytes) / Math.log(unit));
    String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp-1) + (si ? "":"i");
    return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre);
}

在 EB(10^18)之后是 ZB(10^21)。是不是因为 kMGTPE 字符串的越界问题?

并不是。long 的最大值为 2^63-1,大概是 9.2×10^18,所以 long 相对不会超过 EB。

是不是 SI 和二进制的混合问题?不是。前一个版本确实有这个问题,不过很快就修复了。

是不是因为 exp 为 0 会导致 charAt(exp-1) 出错?也不是。第一个 if 语句曾经解决了该状况。exp 值至多为 1。

是不是一些奇怪的舍入问题?对了……

4、许多 9

这段代码在 1MB 之前都十分正确。但当输出为 999,999 时,它(在 SI 模式下)会给出“1000.0 kB”。只管 999,999 与 1,000×1000^1 的间隔比与 999.9×1000^1 的间隔更小,但依据问题的定义,有效数字局部的 1,000 是不正确的。正确后果应为 ”1.0 MB”。

据我所知,原帖下的所有 22 个答案(包含一个应用 Apache Commons 和 Android 库的答案)都有这个问题(或至多是相似的问题)。

那么怎么修复呢?首先,咱们留神到指数(exp)应该在字节数靠近 1 ×1,000^2(1MB)时,将返回后果从 k 改成 M,而不是在字节数靠近 999.9×1000^1(999.9k)时。这个点上的字节数为 999,950。相似地,在超过 999,950,000 时应该从 M 改成 G,以此类推。

为了实现这一点,咱们应该计算该阈值,并当 bytes 大于阈值时减少 exp 的后果。(对于二进制的状况,因为阈值不再是整数,因而须要应用 ceil 进行向上取整)。

if (bytes >= Math.ceil(Math.pow(unit, exp) * (unit - 0.05)))exp++;

5、更多的 9

然而,当输出为 999,949,999,999,999,999 时,后果为 1000.0 PB,而正确的后果为 999.9 PB。从数学上来看这段代码是正确的,那么问题除在何处?

此时咱们曾经达到了 double 类型的精度下限。

对于浮点数运算

依据 IEEE 754 的浮点数示意办法,靠近 0 的数字十分浓密,而很大的数字十分稠密。实际上,超过一半的值位于 - 1 和 1 之间,而且像 Long.MAX_VALUE 如此大的数字对于双精度来说没有任何意义。用代码来示意就是

double a = Double.MAX_VALUE;
double b = a - Long.MAX_VALUE;
System.err.println(a == b);  // prints true

有两个计算是有问题的:

  • String.format 参数中的触发
  • 对 exp 的后果加一时的阈值

当然,改成 BigDecimal 就行了,但这有什么意思呢?而且改成 BigDecimal 代码也会变得更乱,因为规范 API 没有 BigDecimal 的对数函数。

放大两头值

对于第一个问题,咱们能够将 bytes 值放大到精度更好的范畴,并相应地调整 exp。因为最终后果总要取整的,所以抛弃最低位有效数字也无所谓。

if (exp > 4) {
    bytes /= unit;
    exp--;
}

调整最低无效比特

对于第二个问题,咱们须要关怀最低无效比特(999,949,99…9 和 999,950,00…0 等不同幂次的值),所以须要应用不同的办法解决。

首先留神到,阈值有 12 种不同的状况(每个模式下有六种),只有其中一种有问题。有问题的后果的十六进制示意的开端为 D00。如果呈现这种状况,只须要调整至正确的值即可。

long th = (long) Math.ceil(Math.pow(unit, exp) * (unit - 0.05));
if (exp < 6 && bytes >= th - ((th & 0xFFF) == 0xD00 ? 51 : 0))
exp++;

因为须要依赖于浮点数后果中的特定比特模式,所以须要应用 strictfp 来保障它在任何硬件上都能运行正确。

6、负输出

只管还不分明什么状况下会用到负的字节数,但因为 Java 并没有无符号的 long,所以最好解决复数。当初,-10,000 会产生 -10000 B。

引入 absBytes 变量:

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

表达式如此简单,是因为 -Long.MIN_VLAUE == LONG.MIN_VALUE。当前无关 exp 的计算你都要应用 absBytes 来代替 bytes。

7、最终版本

上面是最终版本的代码:

// From: https://programming.guide/worlds-most-copied-so-snippet.html
public static strictfp String humanReadableByteCount(long bytes, boolean si) {
    int unit = si ? 1000 : 1024;
    long absBytes = bytes == Long.MIN_VALUE ? Long.MAX_VALUE : Math.abs(bytes);
    if (absBytes < unit) return bytes + "B";
    int exp = (int) (Math.log(absBytes) / Math.log(unit));
    long th = (long) Math.ceil(Math.pow(unit, exp) * (unit - 0.05));
    if (exp < 6 && absBytes >= th - ((th & 0xFFF) == 0xD00 ? 51 : 0)) exp++;
    String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp - 1) + (si ? "":"i");
    if (exp > 4) {
        bytes /= unit;
        exp -= 1;
    }
    return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre);
}

这个答案最后只是为了防止循环和过多的分支的。讥刺的是,思考到各种边界状况后,这段代码比原答案还难懂了。我必定不会在产品中应用这段代码。

总结

  • Stack Overflow 上的代码就算有几千个赞也可能有问题。
  • 要测试所有边界状况,特地是对于从 Stack Overflow 上复制粘贴的代码。
  • 浮点数运算很难。
  • 复制代码时肯定要注明起源。他人能够据此揭示你重要的事件。

原文链接:https://programming.guide/wor…

作者:Andreas Lundblad

译者:弯月,责编:欧阳姝黎

出品:CSDN(ID:CSDNnews)

近期热文举荐:

1.1,000+ 道 Java 面试题及答案整顿 (2021 最新版)

2. 终于靠开源我的项目弄到 IntelliJ IDEA 激活码了,真香!

3. 阿里 Mock 工具正式开源,干掉市面上所有 Mock 工具!

4.Spring Cloud 2020.0.0 正式公布,全新颠覆性版本!

5.《Java 开发手册(嵩山版)》最新公布,速速下载!

感觉不错,别忘了顺手点赞 + 转发哦!

正文完
 0