• 博客园logo
  • 会员
  • 周边
  • 新闻
  • 博问
  • 闪存
  • 众包
  • 赞助商
  • Chat2DB
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录
思想人生从关注生活开始
博客园    首页    新随笔    联系   管理    订阅  订阅

BitSet使用指南-Java快速入门教程

1. 概述

在本教程中,我们将了解如何使用BitSets来表示位向量。

首先,我们将从不使用布尔数组背后的基本原理开始。然后在熟悉BitSet内部之后,我们将仔细研究它的API。

2. 位数组

为了存储和操作位数组,有人可能会争辩说我们应该使用布尔数组作为我们的数据结构。乍一看,这似乎是一个合理的建议。

但是,布尔数组中的每个布尔成员通常消耗一个字节,而不仅仅是一个位。因此,当我们有严格的内存要求时,或者我们只是为了减少内存占用,布尔数组远非理想。

为了更具体,让我们看看具有 1024 个元素的布尔数组消耗了多少空间:

boolean[] bits = new boolean[1024];
System.out.println(ClassLayout.parseInstance(bits).toPrintable());

理想情况下,我们期望此阵列具有 1024 位内存占用。然而,Java对象布局(JOL)揭示了一个完全不同的现实:

[Z object internals:
 OFFSET  SIZE      TYPE DESCRIPTION            VALUE
      0     4           (object header)        01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4           (object header)        00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4           (object header)        7b 12 07 00 (01111011 00010010 00000111 00000000) (463483)
     12     4           (object header)        00 04 00 00 (00000000 00000100 00000000 00000000) (1024)
     16  1024   boolean [Z.                    N/A
Instance size: 1040 bytes

如果我们忽略对象标头的开销,数组元素将消耗 1024 字节,而不是预期的 1024 位。这比我们预期的内存多 700%。

可寻址性问题和单词撕裂是布尔值不仅仅是一个比特的主要原因。

为了解决这个问题,我们可以使用数值数据类型(如long)和按位运算的组合。这就是BitSet的用武之地。

3.位集的工作原理

 

如前所述,为了实现每个标志一个位的内存使用,BitSetAPI 使用基本数值数据类型和按位运算的组合。

 

为了简单起见,假设我们要用一个字节表示八个标志。首先,我们用零初始化这个单字节的所有位:

初始字节

现在,如果我们想将位置 3 的位设置为true,我们应该首先将数字 1 左移 3:

左移

然后或其结果与当前字节值:

最终或

如果决定将位设置为索引 7,将发生相同的过程:

另一套

如上所示,我们执行七位的左移,并使用or运算符将结果与前一个字节值组合在一起。

 

3.1. 获取位索引

 

要检查特定位索引是否设置为true,我们将使用and运算符。例如,以下是我们如何检查是否设置了索引三:

  1. 对值 1 执行三位的左移
  2. 使用当前字节值和结果
  3. 如果结果大于零,那么我们找到了一个匹配项,并且实际上设置了该位索引。否则,请求的索引为清除或等于false
获取设置套件

上图显示了索引三的获取操作步骤。但是,如果我们查询一个清晰的索引,结果会有所不同:

清除

由于and结果等于零,因此索引 4 是明确的。

3.2. 增加存储

 

目前,我们只能存储 8 位的向量。为了超越这个限制,我们只需要使用一个字节数组,而不是一个字节,就是这样!

现在,每次我们需要设置、获取或清除特定索引时,我们应该首先找到相应的数组元素。例如,假设我们要设置索引 14:

数组集

如上图所示,在找到正确的数组元素后,我们确实设置了适当的索引。

此外,如果我们想在这里设置一个超过 15 的索引,BitSet将首先扩展其内部数组。只有在扩展数组并复制元素后,它才会设置请求的位。这有点类似于ArrayList内部的工作方式。

到目前为止,为了简单起见,我们使用了字节数据类型。但是,BitSetAPI 在内部使用长值数组。

 

4.比特集接口

 

现在我们对这个理论有了足够的了解,是时候看看BitSetAPI是什么样子了。

首先,让我们将BitSet实例的内存占用量与 1024 位的内存占用量与我们之前看到的布尔值进行比较:

BitSet bitSet = new BitSet(1024);

System.out.println(GraphLayout.parseInstance(bitSet).toPrintable());

这将打印BitSet实例的浅大小及其内部数组的大小:

java.util.BitSet@75412c2fd object externals:
          ADDRESS       SIZE TYPE             PATH         VALUE
        70f97d208         24 java.util.BitSet              (object)
        70f97d220        144 [J               .words       [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

如上所示,它在内部使用带有 16 个元素(16 * 64 位 = 1024 位)的long[]。无论如何,这个实例总共使用了 168 个字节,而布尔数组使用了 1024 个字节。

我们拥有的位数越多,占用空间差异就越大。例如,要存储 1024 * 1024 位,布尔 [] 消耗 1 MB,BitSet实例消耗大约 130 KB。

4.1. 构造位集

 

创建BitSet实例的最简单方法是使用no-arg 构造函数:

BitSet bitSet = new BitSet();

这将创建一个大小为 1 的长 [] 的BitSet实例。当然,如果需要,它可以自动增长此数组。

也可以创建具有初始位数的BitSet:

 
BitSet bitSet = new BitSet(100_000);

在这里,内部数组将有足够的元素来容纳 100,000 位。当我们已经对要存储的位数有一个合理的估计时,这个构造函数会派上用场。在这样的用例中,它可以防止或减少在增长数组元素时不必要的复制。

甚至可以从现有的long[]、byte[]、LongBuffer 和ByteBuffer 创建 BitSet。例如,这里我们从给定的long[] 创建一个BitSet实例:

BitSet bitSet = BitSet.valueOf(new long[] { 42, 12 });

还有三个重载版本的valueOf()静态工厂方法来支持其他提到的类型。

4.2. 设置位

 

我们可以使用set(index)方法将特定索引的值设置为true:

BitSet bitSet = new BitSet();

bitSet.set(10);
assertThat(bitSet.get(10)).isTrue();

像往常一样,索引从零开始。甚至可以使用set(fromInclusive, toExclusive) 方法将一系列位设置为true:

bitSet.set(20, 30);
for (int i = 20; i <= 29; i++) {
    assertThat(bitSet.get(i)).isTrue();
}
assertThat(bitSet.get(30)).isFalse();

从方法签名中可以明显看出,开始索引是包含索引的,结束索引是独占索引。

当我们说设置索引时,我们通常是指将其设置为true。尽管有这个术语,我们可以使用set(index, boolean) 方法将特定的位索引设置为false:

bitSet.set(10, false);
assertThat(bitSet.get(10)).isFalse();

此版本还支持设置一系列值:

 
bitSet.set(20, 30, false);
for (int i = 20; i <= 29; i++) {
    assertThat(bitSet.get(i)).isFalse();
}

4.3. 清除位

 

我们可以简单地使用clear(index) 方法清除它,而不是将特定的位索引设置为false:

bitSet.set(42);
assertThat(bitSet.get(42)).isTrue();
        
bitSet.clear(42);
assertThat(bitSet.get(42)).isFalse();

此外,我们还可以使用clear(fromInclusive,toExclusive)重载版本清除一系列位:

bitSet.set(10, 20);
for (int i = 10; i < 20; i++) {
    assertThat(bitSet.get(i)).isTrue();
}

bitSet.clear(10, 20);
for (int i = 10; i < 20; i++) {
    assertThat(bitSet.get(i)).isFalse();
}

有趣的是,如果我们调用此方法而不传递任何参数,它将清除所有设置位:

bitSet.set(10, 20);
bitSet.clear();
for (int i = 0; i < 100; i++) { 
    assertThat(bitSet.get(i)).isFalse();
}

如上所示,调用clear() 方法后,所有位都设置为零。

4.4. 获取位

 

到目前为止,我们广泛使用了get(index)方法。设置请求的位索引后,此方法将返回true。否则,它将返回false:

bitSet.set(42);

assertThat(bitSet.get(42)).isTrue();
assertThat(bitSet.get(43)).isFalse();

与set和clear 类似,我们可以使用get(fromInclusive, toExclusive) 方法获取一系列位索引:

bitSet.set(10, 20);
BitSet newBitSet = bitSet.get(10, 20);
for (int i = 0; i < 10; i++) {
    assertThat(newBitSet.get(i)).isTrue();
}

如上所示,此方法返回当前 [20, 30) 范围内的另一个BitSet。也就是说,位集变量的索引 20 等效于newBitSet变量的索引零。

4.5. 翻转位

 

要否定当前的位索引值,我们可以使用flip(index)方法。也就是说,它会将真值转换为假值,反之亦然:

 
bitSet.set(42);
bitSet.flip(42);
assertThat(bitSet.get(42)).isFalse();

bitSet.flip(12);
assertThat(bitSet.get(12)).isTrue();

类似地,我们可以使用flip(fromInclusive, toExclusive) 方法对一系列值实现相同的操作:

bitSet.flip(30, 40);
for (int i = 30; i < 40; i++) {
    assertThat(bitSet.get(i)).isTrue();
}

4.6. 长度

 

BitSet 有三种类似长度的方法。size() 方法返回内部数组可以表示的位数。例如,由于 no-arg 构造函数为一个带有一个元素的长 [] 数组分配,因此size() 将为其返回 64:

BitSet defaultBitSet = new BitSet();
assertThat(defaultBitSet.size()).isEqualTo(64);

对于一个 64 位数字,我们只能表示 64 位。当然,如果我们显式传递位数,这将改变:

BitSet bitSet = new BitSet(1024);
assertThat(bitSet.size()).isEqualTo(1024);

此外,基数()方法表示BitSet中设置位数:

assertThat(bitSet.cardinality()).isEqualTo(0);
bitSet.set(10, 30);
assertThat(bitSet.cardinality()).isEqualTo(30 - 10);

起初,此方法返回零,因为所有位都是假的。将 [10, 30) 范围设置为true 后,基数()方法调用返回 20。

此外,length() 方法在最后一个设置位的索引之后返回一个索引:

assertThat(bitSet.length()).isEqualTo(30);
bitSet.set(100);
assertThat(bitSet.length()).isEqualTo(101);

起初,最后一组索引是 29,因此此方法返回 30。当我们将索引 100 设置为 true 时,length() 方法返回 101。还值得一提的是,如果所有位都清除,此方法将返回零。

最后,当BitSet 中至少有一个设置位时,isEmpty() 方法返回false。否则,它将返回true:

 
assertThat(bitSet.isEmpty()).isFalse();
bitSet.clear();
assertThat(bitSet.isEmpty()).isTrue();

4.7. 与其他位集组合

 

intersects(BitSet)方法采用另一个BitSet,并在两个BitSet具有共同点时返回true。也就是说,它们在同一索引中至少有一个设置位:

BitSet first = new BitSet();
first.set(5, 10);

BitSet second = new BitSet();
second.set(7, 15);

assertThat(first.intersects(second)).isTrue();

[7, 9] 范围在两个位集中都设置,因此此方法返回true。

也可以在两个位集上执行逻辑和操作:

first.and(second);
assertThat(first.get(7)).isTrue();
assertThat(first.get(8)).isTrue();
assertThat(first.get(9)).isTrue();
assertThat(first.get(10)).isFalse();

这将在两个位集之间执行逻辑和,并用结果修改第一个变量。类似地,我们也可以在两个位集上执行逻辑异或:

first.clear();
first.set(5, 10);

first.xor(second);
for (int i = 5; i < 7; i++) {
    assertThat(first.get(i)).isTrue();
}
for (int i = 10; i < 15; i++) {
    assertThat(first.get(i)).isTrue();
}

还有其他方法,如andNot(BitSet)或or(BitSet),它们可以在两个BitSet上执行其他逻辑操作。

4.8. 其他

 

从Java 8开始,有一个stream()方法来流式传输BitSet的所有集合位。例如:

BitSet bitSet = new BitSet();
bitSet.set(15, 25);

bitSet.stream().forEach(System.out::println);

这会将所有设置的位打印到控制台。由于这将返回一个IntStream,我们可以执行常见的数值运算,例如求和、平均值、计数等。例如,这里我们正在计算设置位数:

assertThat(bitSet.stream().count()).isEqualTo(10);

此外,nextSetBit(fromIndex)方法将返回从fromIndex开始的下一个设置位索引:

 
assertThat(bitSet.nextSetBit(13)).isEqualTo(15);

fromIndex本身也包含在此计算中。当BitSet 中没有任何真正的位时,它将返回 -1:

assertThat(bitSet.nextSetBit(25)).isEqualTo(-1);

类似地,nextClearBit(fromIndex)返回从fromIndex开始的下一个清除索引:

assertThat(bitSet.nextClearBit(23)).isEqualTo(25);

另一方面,previousClearBit(fromIndex)返回最近一个清除索引的索引,方向相反:

assertThat(bitSet.previousClearBit(24)).isEqualTo(14);

previousSetBit(fromIndex)也是如此:

assertThat(bitSet.previousSetBit(29)).isEqualTo(24);
assertThat(bitSet.previousSetBit(14)).isEqualTo(-1);

此外,我们可以分别使用 toByteArray() 或toLongArray() 方法将BitSet转换为byte[] 或long[]:

byte[] bytes = bitSet.toByteArray();
long[] longs = bitSet.toLongArray();

5. 结论

 

在本教程中,我们了解了如何使用BitSets 来表示位向量。

起初,我们熟悉了不使用布尔数组来表示位向量的基本原理。然后我们看到了BitSet的内部工作方式及其 API 的外观。

posted @ 2023-02-24 12:11  JackYang  阅读(1287)  评论(1)    收藏  举报
刷新页面返回顶部
博客园  ©  2004-2026
浙公网安备 33010602011771号 浙ICP备2021040463号-3