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

BitSet.size() returns negative value. Known bug?

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
    }
}

在测试系统上:

$ 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)

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

我怀疑这会被记录下来。它肯定不会 'fixed',因为没有不破坏向后兼容性的合理修复可用,而且它远没有足够的相关性来采取如此激烈的步骤。

深入挖掘 - 为什么会这样?

虽然 API 文档没有做出这样的保证,但 size() 效果 只是 return 是 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

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

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

然后它还解释了为什么你正在见证你现在的样子:Integer.MAX_VALUE 是 2147483647。将其四舍五入到最接近的 64 的倍数,你会得到... 2147483648。溢出 int - 和 Integer.MAX_VALUE + 1 / (int) 2147483648L - 都是相同的值:-2147483648。即 one 值存在于 signed int space 中作为负数没有匹配的正数(这也有意义:一些位序列需要表示0 既不是正数也不是负数。按照惯例/根据 2s 补码的规则,这就是 java 以位形式表示所有数字的方式,0 在 'positive' space 中(给定都是 0 位)。因此 'leaches' 从那里得到一个数字,那个数字是 2147483648。

让我们修复它!

一个简单的解决方法是用 size() 方法 return 代替 long,它可以简单地表示 2147483648,这是正确答案。不幸的是,这不向后兼容。因此,如果有人要求进行更改,则极不可能成功。

另一个修复方法是用一些 throw-in-the-towel 名称创建第二个方法,例如 accurateSize() 或诸如此类的东西,这样 size() 就不会受到干扰,从而保留向后兼容性,这确实 return long。但这会永远弄脏 API,因为除了您可以要求的最大 63 个数字之外,这个细节与所有情况都不相关。 (Integer.MAX_VALUE-62 到 Integer.MAX_VALUE 是您可以为 nBits 传递的唯一值,这会导致 size() returning 负值。负值 returned永远是 Integer.MIN_VALUE。我怀疑他们会那样做。

第三个解决方法是撒谎 return Integer.MAX_VALUE,这不是正确的值(因为实际上 'available' 位 space).鉴于您实际上不能 'set' 该位值,因为您不能将 2147483648 传递给构造函数(因为您必须传递一个 int,如果您尝试你最终得到 -2147483648,它是负数并导致构造函数抛出,因此没有给你一个实例:如果没有 hackery,例如使用反射来设置私有字段,APIs 不需要地址,你可以制作一个实际上可以 存储 第 2147483648 位的值的 BitSet。

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

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

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

size() 实际上并没有损坏。 returned 这个数字是完全正确的:也就是说,实际上是大小。只是当你打印它时,它 'prints wrong' - 因为 size() 的 return 值应该被解释为 unsignedsize() 的 javadoc 明确指出了它的唯一要点,即取该数字并减去 1:这将告诉您可以设置的最大元素。

这很好用:

BitSet x = new BitSet(Integer.MAX_VALUE);
int maxIndex = x.size() - 1;
System.out.println(maxIndex);
x.set(maxIndex);

以上代码运行良好。 maxIndex 值如预期的那样是 2147483647(即 Integer.MAX_VALUE)。

因此,这里真的没有什么可做的:API 就这样很好,并且按照它的建议准确地使用它。任何 API 你想要的 'better' 都是向后不兼容的;更改 BitSet 不是一个好主意,添加更多的方法,java.util.Vector 样式会使 API 变丑,这绝对是治愈比疾病更糟糕的情况。

只是留下了向文档添加注释。如果你在文档中深入研究这种级别的外来事物,你最终会得到大量的文档,这又是一种比疾病更糟糕的治疗方法。可持续的解决方案也许javadoc 获得编写深奥脚注的基本能力,例如javadoc 工具可以通过默认折叠的 'folding' 弹出界面元素变成 HTML(即不可见的奇异脚注),但如果你真的想要阅读详情。

Javadoc 没有这个。

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