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

2023-11-01 05:10

本文主要是介绍java 复制粘贴的代码_复制粘贴一时爽:传播最广的一段 Java 代码曝出 Bug,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

复制粘贴一时爽,频出 bug 火葬场。对开发者而言,Stack Overflow 和 GitHub 是最为熟悉不过的两大平台,这些平台充斥着大量开源项目信息和解决各类问题的代码片段。最近,一位叫做 Aioobe 的开发者在一项调查中发现了一段自己十年前写的代码,这段代码成为了 Stack Overflow 上复制次数最多、传播范围最广的答案,GitHub 的众多项目中也存在这段代码。然而,这位开发者表示这段代码其实是有 bug 的,并于近日更新了答案并作出说明。

这段代码是干啥的?

2010 年的时候,我整天泡在 Stack Overflow 上回答问题,希望可以提高自己的知名度。当时,有一个问题吸引了我的注意:如何以人类可读的格式输出字节数?举个例子,将“123456789 字节”转换为“123.5 MB”的格式输出。

f8db0063de0f1f47adb57dff7e6f37dd.png

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

这里的隐含范式在于所得到的字符串值应该在 1 到 999.9 之间,后面再跟上一个大小合适的单位。当时已经有人给了一条回应。答案中的代码以循环为基础,基本思路非常简单:尝试所有单位,从最大(EB,即 1018 字节)到最小(B,即 1 字节),而后使用一种显示数量小于实际字节数量的单位。用伪代码写出来,基本是这么个意思:

复制代码

suffixes = [ "EB", "PB", "TB", "GB", "MB", "kB", "B" ]magnitudes = [ 1018, 1015, 1012, 109, 106, 103, 100 ]i = 0while (i < magnitudes.length && magnitudes[i] > byteCount) i++printf("%.1f %s", byteCount / magnitudes[i], suffixes[i])一般来说,如果发布的正确答案已经获得了正分数,那后发者很难追上。在 Stack Overflow 上,这就叫“拔枪最快的赢”。不过,我认为这个答案有缺陷,所以准备重新改改。我意识到,无论是 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 也可能在一定程度上影响执行效率,但至少这里没有循环,几乎不涉及分支,我觉得还是比较整洁的。

这里面使用的数学方法非常简单。字节计数表示为 byeCount=1000s , 其中的 s 代表小数点后的位数(以二进制表示,则使用 1024 为基数),求解 s,即可得出 s=log1000(byteCount)。

API 里没有现成的 log1000 可以直接使用,但我们不妨用自然对数来表示,即 s=log(byteCount)/log(1000)。接下来,我们取 s 的底(即取整数),因为假如我们得出的结果超过 1 MB(但不足 1 GB),则希望继续使用 MB 作为表示单位。

此时,如果 s=1,则单位为 KB;如果 s=2,则单位为 MB;依此类推,我们将 byteCount 值除以 1000s ,然后取对应的单位。

接下来,我能做的就是等待,看看社区是否喜欢这个答案。那时候的我,绝对想不到它会成为 Stack Overflow 上复制最多的代码片段。

BUG 在哪?

估计不少人看到这儿肯定在想,这段代码里到底有什么 bug?

再来看一遍代码:

复制代码

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,即 1018 之后,接下来的单位应该是 ZB,即 1021。难道是输入量过大导致“kMGTPE”字符串的索引超出范围?不是的,long 的最大值是 263-1≈9.2×1018,因此任何 long 值都不会超出 EB 范围。

那么,是 SI 与二进制之间存在混杂吗?也不是。答案的早期版本中确实有这个问题,但很快就得到了修复。

那么,是不是 exp 可以为 0 会导致 charAt(exp-1) 发生错误?不是的。第一个 if 语句也涵盖了这种情况,因此 exp 值将始终至少为 1。

那就只剩最后一种情况了,输出结果中是否存在某些奇怪的舍入错误?这正是我们接下来要讨论的部分……

太多个 9

这套解决方案一直运作良好,直到字节数量达到 1 MB。假定输入为 999999 字节,那么结果(在 SI 模式下)将为“1000.0 kB”。尽管 999999 比 999.9 x 10001 更接近于 1000 x 10001,但根据规范,1000 的“有效位数”超出了范围。正确的结果应该是“1.0 MB”。

无论如何,在这个帖子的所有 22 个答案中(包括使用 Apache Commons 以及 Android 库的答案)截至本文撰稿之时都存在这个错误(或者其变体)。那么,我们该如何解决?

首先,我们会注意到,一旦字节数比 999.9 x 10001(999.9 k)更接近于 1 x 10002(1 MB),则指数(exp)就应由“k”变更为“M”。例如,9999950 就符合这种情况。同样的,当我们输入 999950000 时,我们应该从“M”切换为“G”,依此类推。为了达成这一目标,我们会计算该阈值,一旦字节数超过阈值,则增加 exp:

复制代码

if (bytes >= Math.pow(unit, exp) * (unit - 0.05)) 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(l1 == l2); // prints true下面来看两项有问题的计算:

String.format 参数中的除法;exp 进位阈值我们当然可以切换为 BigDecimal,但这么干就没意思了。另外,由于标准 API 中没有 BigDecimal log 函数,所以问题其实仍然存在。

缩小中间值

对于第一个问题,我们可以将字节值缩小至更合理的精度范围,同时相应调整 exp。无论如何,最终结果都会四舍五入,因此我们要做的就是不要舍弃最低有效数字。

复制代码

if (exp > 4) { bytes /= unit; exp--;}调整最低有效位

对于第二个问题,我们当然关心最低有效位(999、949、99…9 与 999,950,00…0 应该以不同的单位结尾),因此必须得想个不同的解决方案。

首先,我们注意到阈值存在 12 种不同的可能值(每种模式 6 种),而且其中只有一种最终会发生故障。通过以 D0016 结尾这一迹象,可以准确识别出错误结果。一旦发生这种情况,我们将其调整为正确值即可。

复制代码

long th = (long) (Math.pow(unit, exp) * (unit - 0.05));if (exp < 6 && bytes >= th - ((th & 0xFFF) == 0xD00 ? 52 : 0)) exp++;由于我们在浮点结果中需要使用特定数位模式,因此下手的对象自然就是 strictfp,旨在保证其不受硬件运行代码的影响。

负输入

目前我还没想到什么情况下有可能需要使用负字节数量,但考虑到 Java 不支持无符号 long,我们最好还是把这个问题考虑进来。现在,如果输入为 -10000,那么结果为 -10000 B。这里我们引入 absBytes:

复制代码

long absBytes = bytes == Long.MIN_VALUE ? Long.MAX_VALUE : Math.abs(bytes);这里的表达之所以如此复杂,是基于 -Long.MIN_VALUE == Long.MIN_VALUE 这一事实。现在,我们利用 absBytes 替代 bytes 执行所有与 exp 相关的计算。

最终版本

以下是代码片段的最终版本,其中已经对最初版本做了精心调整与改进:

复制代码

// 来自: https://programming.guide/the-worlds-most-copied-so-snippet.htmlpublic 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.pow(unit, exp) * (unit - 0.05)); if (exp < 6 && absBytes >= th - ((th & 0xfff) == 0xd00 ? 52 : 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);}请注意,这段代码最初的目标是避免由循环以及大量分支带来的复杂性。在解决了所有极端情况之后,新代码的可读性要比原始版本更差。我个人是肯定不会把这段代码复制到生产代码中的。

这段代码被复制到了哪里?

2018 年,一位名叫 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。

96b5e8c44fc58383659247fdd666cec6.png

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

复制代码

$ git grep humanReadableByteCount心得摘要

最后,我希望告诉广大开发者 Stack Overflow 上的代码片段可能存在 bug,即使得到无数好评也改变不了这一事实;一定要对所有极端情况做出测试,特别是测试那些复制自 Stack Overflow 的代码;浮点运算很复杂,也很困难,在复制代码时,请确保了解代码背后的逻辑和使用规范。

这篇关于java 复制粘贴的代码_复制粘贴一时爽:传播最广的一段 Java 代码曝出 Bug的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/320691

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

活用c4d官方开发文档查询代码

当你问AI助手比如豆包,如何用python禁止掉xpresso标签时候,它会提示到 这时候要用到两个东西。https://developers.maxon.net/论坛搜索和开发文档 比如这里我就在官方找到正确的id描述 然后我就把参数标签换过来