BitSet.size() 返回负值。已知错误?

Dan*_* S. 4 java debugging bitset

new BitSet(Integer.MAX_VALUE).size()报告负值:

import java.util.BitSet;

public class NegativeBitSetSize {
    public static void main(String[] args) {
        BitSet a;

        a = new BitSet(Integer.MAX_VALUE);
        System.out.println(a.size()); // -2147483648

        a = new BitSet(Integer.MAX_VALUE - 50);
        System.out.println(a.size()); // -2147483648

        a = new BitSet(Integer.MAX_VALUE - 62);
        System.out.println(a.size()); // -2147483648

        a = new BitSet(Integer.MAX_VALUE - 63);
        System.out.println(a.size()); // 2147483584
    }
}
Run Code Online (Sandbox Code Playgroud)

在测试系统上:

$ java -version
openjdk version "11.0.14" 2022-01-18
OpenJDK Runtime Environment (build 11.0.14+9-Ubuntu-0ubuntu2.18.04)
OpenJDK 64-Bit Server VM (build 11.0.14+9-Ubuntu-0ubuntu2.18.04, mixed mode, sharing)
Run Code Online (Sandbox Code Playgroud)

我找不到这方面的错误报告。这是已知的或有记录的吗?

rzw*_*oot 7

我怀疑这是否会被记录下来。它肯定不会被“修复”,因为没有任何明智的修复可以不破坏向后兼容性,而且它还远没有足够的相关性来采取如此激烈的步骤。

深入探究——为什么会发生这种情况?

虽然 API 文档没有做出这样的保证,但其效果size()它只是返回nBits您在构造BitSet实例时传递的值...但向上舍入到下一个可被 64 整除的值:

sysout(new BitSet(1).size());   // 64
sysout(new BitSet(63).size());  // 64
sysout(new BitSet(64).size());  // 64
sysout(new BitSet(65).size());  // 128
sysout(new BitSet(100).size()); // 128
sysout(new BitSet(128).size()); // 128
sysout(new BitSet(129).size()); // 192
Run Code Online (Sandbox Code Playgroud)

这是符合逻辑的;该实现使用一个值数组long来存储这些位(因为这比使用 a 更有效(8 倍!)boolean[],因为每个布尔值仍然占用数组中的一个字节,并且整个 long 的位作为单独的位)多变的)。

该规范并不保证这一点,但它解释了为什么会发生这种情况。

然后,它还解释了为什么您正在目睹自己的身份:Integer.MAX_VALUE是 2147483647。将其四舍五入到最接近的 64 倍数,然后您得到... 2147483648。其中溢出int- 和Integer.MAX_VALUE + 1/ (int) 2147483648L- 都是相同的值:-2147483648。这是在有符号空间中作为负数存在的一个int值,没有匹配的正数(这也是有道理的:某些位序列需要表示既不是正数也不是负数的 0。按照惯例/按照 2 补码的规则,其中java 以位形式表示所有数字的方式,0 位于“正”空间中(假设它全是 0 位)。因此,它从那里“浸出”一个数字,该数字是 2147483648。

我们来解决它吧!

一个简单的解决方法是让该size()方法返回 a long,它可以简单地表示 2147483648,这是正确的答案。不幸的是,这不向后兼容。因此,如果有人要求这一改变,极不可能成功。

另一个修复方法是创建第二个方法,使用一些认输的名称,例如accurateSize()或 等等,这样就size()可以保持不受干扰,从而保留向后兼容性,这确实会返回long. 但这会永远弄脏 API,因为除了您可以要求的最大 63 个数字之外,这个细节与所有情况都无关。(Integer.MAX_VALUE-62 到 Integer.MAX_VALUE 是唯一可以传递给 nBits 的值,这会导致size()返回负值。返回的负值将始终为Integer.MIN_VALUE。我怀疑他们会这样做。

第三个修复方法是撒谎并返回 Integer.MAX_VALUE,这不是完全正确的值(因为实际上在位空间中“可用”多了 1 位)。鉴于您实际上无法“设置”该位值,因为您无法将 2147483648 传递给构造函数(因为您必须传递一个int,该数字不能作为 int 传递,如果您尝试最终得到 -2147483648,这是负值,会导致构造函数抛出异常,因此不会给您一个实例:如果没有黑客技术,例如使用反射来设置私有字段(API 不需要解决这些问题),您就无法创建一个可以实际存储的BitSet第 2147483648 位。

这让我们明白了重点size()是什么。是为了告诉你BitSet对象占用了多少字节吗?如果这就是重点,那么它从来都不是解决这个问题的好方法:JVM 不保证 along[]的内存大小为 arrSize*8 字节(尽管所有 JVM 实现都有这一点,+ 数组头结构的一些低开销)。

相反,它可能只是让您知道可以用它做什么。即使您调用,比如说,new BitSet(5)您仍然可以设置第 6 位(因为为什么不呢 - 它不会“花费”任何东西,我想这就是意图)。您可以设置从 0 到.size()负 1 的所有位。

这让我们得到了真正的答案!

size() 实际上并没有被破坏。返回的数字完全正确:实际上就是大小。只是当您打印它时,它“打印错误” - 因为size()的返回值应该被解释为unsigned。javadocsize()明确指出了它唯一的一点,即获取该数字,然后减去 1:然后这会告诉您可以设置的最大元素。

这工作得很好

BitSet x = new BitSet(Integer.MAX_VALUE);
int maxIndex = x.size() - 1;
System.out.println(maxIndex);
x.set(maxIndex);
Run Code Online (Sandbox Code Playgroud)

上面的代码工作正常。正如预期,maxIndex 值为 2147483647(即 Integer.MAX_VALUE)。

因此,这里实际上没有什么可做的:API 本身就很好,并且按照它建议您准确使用它的方式进行操作。您想提出的任何“更好”的 API 都将向后不兼容;改变 BitSet 并不是一个好主意,添加更多的方法、java.util.Vector风格会使 API 变得丑陋,这绝对是治本不如治病的情况。

只需在文档中添加注释即可如果你深入研究文档中的这种水平的外来事物,你最终会得到大量的文档,而这些文档又是比疾病更糟糕的治疗方法。可持续的解决方案也许是让 javadoc 获得编写深奥脚注的基本能力,例如,该javadoc工具可以通过默认情况下折叠起来的“折叠”弹出界面元素将其转换为 HTML(即外来脚注不可见) ),但如果您确实想阅读详细信息,可以扩展。

Javadoc没有这个。

结论:人们可以很容易地认为 API 根本没有损坏;没有size()明确说明返回值应解释为有符号 int;唯一明确的承诺是您可以从结果中减去 1 并将其用作索引,这样效果很好。充其量,您可以提交错误报告来更新文档,但这不是一个好主意,因为不可能(轻松)将此类深奥内容添加到文档中。如果您确实想走这条路,JDK 库中还有很多此类内容也没有记录。