Java BitSet类解析:高效位向量实现

java.util.BitSet 类实现了一个根据需要增长的位向量。它的核心思想是将一系列的布尔值(位)紧凑地存储在一个 long 类型的数组中,每个 long 值可以存储 64 个位。

Java的BitSet没有使用 isFlipped标记,因此flip和count的复杂度不是O(1)的,但这使得实现变得简单和清晰。

类结构和关键字段

BitSet.java

java 复制代码
public class BitSet implements Cloneable, java.io.Serializable {
    /*
     * BitSets are packed into arrays of "words."  Currently a word is
     * a long, which consists of 64 bits, requiring 6 address bits.
     * The choice of word size is determined purely by performance concerns.
     */
    private static final int ADDRESS_BITS_PER_WORD = 6; // 每个word的地址位数 (2^6 = 64)
    private static final int BITS_PER_WORD = 1 << ADDRESS_BITS_PER_WORD; // 每个word的位数 (64)
    private static final int BIT_INDEX_MASK = BITS_PER_WORD - 1; // 用于获取位在word内的索引的掩码 (63, 即 0x3F)

    /* Used to shift left or right for a partial word mask */
    private static final long WORD_MASK = 0xffffffffffffffffL; // 全1的掩码,用于操作整个word

    /**
     * @serialField bits long[]
     *
     * The bits in this BitSet.  The ith bit is stored in bits[i/64] at
     * bit position i % 64 (where bit position 0 refers to the least
     * significant bit and 63 refers to the most significant bit).
     */
    @java.io.Serial
    private static final ObjectStreamField[] serialPersistentFields = {
        new ObjectStreamField("bits", long[].class),
    };

    /**
     * The internal field corresponding to the serialField "bits".
     */
    private long[] words; // 存储位的long数组

    /**
     * The number of words in the logical size of this BitSet.
     */
    private transient int wordsInUse = 0; // 实际使用的word数量,表示BitSet的逻辑大小

    /**
     * Whether the size of "words" is user-specified.  If so, we assume
     * the user knows what he's doing and try harder to preserve it.
     */
    private transient boolean sizeIsSticky = false; //标记words数组的大小是否由用户指定

    /* use serialVersionUID from JDK 1.0.2 for interoperability */
    @java.io.Serial
    private static final long serialVersionUID = 7997698588986878753L;

    // ... 其他方法和字段 ...
}

关键点解释:

  1. words (long[]) : 这是 BitSet 内部存储数据的核心。每个 long 元素被称为一个 "word",可以存储 64 个位。第 i 个位存储在 words[i/64] 的第 i % 64 个比特位上(最低有效位是第0位)。
  2. ADDRESS_BITS_PER_WORD (int) : 固定为 6。因为一个 long 是 64 位,而 2^6 = 64,所以需要 6 位来寻址一个 long 中的具体某一位。
  3. BITS_PER_WORD (int) : 固定为 64,即一个 long 包含的位数。
  4. BIT_INDEX_MASK (int) : 值为 63 (二进制 00111111)。用于通过位与操作 (& BIT_INDEX_MASK) 快速计算一个位索引在其所属 long word 内的偏移量(相当于 bitIndex % 64)。
  5. WORD_MASK (long) : 值为 0xffffffffffffffffL (全1)。用于对整个 long word 进行操作,例如全部置位或全部清零。
  6. wordsInUse (transient int) : 表示 words 数组中当前实际被用来存储有效位的 long 元素的数量。这是 BitSet 的逻辑大小。例如,如果最高的被设置的位是第 100 位,那么 wordsInUse 至少是 wordIndex(100) + 1。这个字段是 transient 的,因为它可以在序列化和反序列化时重新计算。
  7. sizeIsSticky (transient boolean) : 如果用户在构造 BitSet 时指定了初始大小 (nbits),这个标志位会设为 true。在这种情况下,BitSet 会尽量保持 words 数组的大小,避免不必要的缩小。
  8. serialPersistentFields : 定义了 BitSet 对象在序列化时需要持久化的字段,这里只有 bits (即 words 数组)。

构造函数

  • public BitSet():

    BitSet.java

    java 复制代码
    public BitSet() {
        initWords(BITS_PER_WORD); // 初始化words数组,默认大小为一个word (64位)
        sizeIsSticky = false;
    }

    默认构造函数创建一个初始容量为 64 位的 BitSet

  • public BitSet(int nbits):

    BitSet.java

    java 复制代码
    public BitSet(int nbits) {
        // nbits can't be negative; size 0 is OK
        if (nbits < 0)
            throw new NegativeArraySizeException("nbits < 0: " + nbits);
    
        initWords(nbits); // 根据nbits初始化words数组
        sizeIsSticky = true; // 用户指定了大小
    }

    允许用户指定初始可以表示的位数。initWords 会计算需要多少个 long 来存储 nbits

  • private void initWords(int nbits):

    BitSet.java

    java 复制代码
    private void initWords(int nbits) {
        words = new long[wordIndex(nbits-1) + 1];
    }

    这个方法根据需要的位数 nbits 来初始化 words 数组。wordIndex(nbits-1) 计算出存储第 nbits-1 位所需要的 word 的索引,然后加 1 得到数组的长度。如果 nbits 为 0,nbits-1 为 -1,wordIndex(-1) 会根据右移操作的特性得到 -1,所以 words 数组长度为 0。

wordIndex

  • private static int wordIndex(int bitIndex) :

    BitSet.java

    java 复制代码
    private static int wordIndex(int bitIndex) {
        return bitIndex >> ADDRESS_BITS_PER_WORD; // 相当于 bitIndex / 64
    }

    计算给定 bitIndex 所在的 long word 在 words 数组中的索引。通过右移 ADDRESS_BITS_PER_WORD (即 6) 位实现,这等价于除以 64。

容量管理

  • private void ensureCapacity(int wordsRequired):

    BitSet.java

    java 复制代码
    private void ensureCapacity(int wordsRequired) {
        if (words.length < wordsRequired) {
            // Allocate larger of doubled size or required size
            int request = Math.max(2 * words.length, wordsRequired);
            words = Arrays.copyOf(words, request); // 扩展数组
            sizeIsSticky = false; // 自动扩展后,不再认为是用户指定大小
        }
    }

    当需要的 long word 数量 (wordsRequired) 超过当前 words 数组的长度时,此方法会扩展数组。新的大小通常是当前大小的两倍和 wordsRequired 中的较大者。

  • private void expandTo(int wordIndex):

    java 复制代码
    private void expandTo(int wordIndex) {
        int wordsRequired = wordIndex+1;
        if (wordsInUse < wordsRequired) {
            ensureCapacity(wordsRequired); // 确保容量足够
            wordsInUse = wordsRequired;    // 更新wordsInUse
        }
    }

    确保 BitSet 至少可以容纳到 wordIndex 所指定的 word。如果当前的 wordsInUse 小于需要的 wordsRequired,它会调用 ensureCapacity 来扩展 words 数组,并更新 wordsInUse。这个方法可能会临时破坏不变性条件(例如,新扩展的 word 可能为0,但 wordsInUse 已经指向它),调用者需要负责恢复。

维护 wordsInUse 和不变性

  • private void recalculateWordsInUse():

    BitSet.java

    java 复制代码
    private void recalculateWordsInUse() {
        // Traverse the bitset until a used word is found
        int i;
        for (i = wordsInUse-1; i >= 0; i--)
            if (words[i] != 0)
                break;
    
        wordsInUse = i+1; // The new logical size
    }

    当某些操作(如 clearflip)可能导致最高的置位被清除时,调用此方法。它从 wordsInUse-1 开始向前遍历 words 数组,找到最后一个不为0的 word,然后更新 wordsInUse 为该 word 的索引加1。

  • private void checkInvariants():

    BitSet.java

    java 复制代码
    private void checkInvariants() {
        assert(wordsInUse == 0 || words[wordsInUse - 1] != 0); // 如果wordsInUse > 0,最后一个使用的word必须非0
        assert(wordsInUse >= 0 && wordsInUse <= words.length); // wordsInUse必须在有效范围内
        assert(wordsInUse == words.length || words[wordsInUse] == 0); // 如果wordsInUse < words.length,则wordsInUse位置的word必须为0
    }

    这是一个断言方法,用于在开发和测试期间检查 BitSet 内部状态的一致性。

    • 第一个断言确保如果 wordsInUse 大于0,那么 words[wordsInUse - 1] (即逻辑上的最后一个word) 必须是非零的。
    • 第二个断言确保 wordsInUse 不会超出 words 数组的边界。
    • 第三个断言确保如果 wordsInUse 没有达到 words 数组的末尾,那么 words[wordsInUse] (即逻辑大小之外的第一个word) 必须是0。

核心位操作方法

  • public void set(int bitIndex):

    BitSet.java

    java 复制代码
    public void set(int bitIndex) {
        if (bitIndex < 0)
            throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex);
    
        int wordIndex = wordIndex(bitIndex);
        expandTo(wordIndex); // 确保能容纳该位
    
        words[wordIndex] |= (1L << bitIndex); // 使用位或操作设置位
    
        checkInvariants(); // 检查不变性
    }

    将指定索引的位设置为 true

    1. 计算 wordIndex
    2. 调用 expandTo(wordIndex) 确保 words 数组足够大,并且 wordsInUse 被相应更新。
    3. 通过位或操作 |=words[wordIndex] 中对应 bitIndex % 64 的位设置为1。表达式 (1L << bitIndex) 会自动处理 bitIndex % 64,因为 long 左移超过63位时,实际移位数是 bitIndex % 64
  • public void clear(int bitIndex):

    BitSet.java

    java 复制代码
    public void clear(int bitIndex) {
        if (bitIndex < 0)
            throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex);
    
        int wordIndex = wordIndex(bitIndex);
        if (wordIndex >= wordsInUse) // 如果位超出了当前逻辑大小,则它已经是false,无需操作
            return;
    
        words[wordIndex] &= ~(1L << bitIndex); // 使用位与非操作清除位
    
        recalculateWordsInUse(); // 可能清除了最高的置位,需要重新计算wordsInUse
        checkInvariants();
    }

    将指定索引的位设置为 false

    1. 如果 wordIndex 超出 wordsInUse,说明该位已经是 false,直接返回。
    2. 通过位与非操作 &= ~words[wordIndex] 中对应 bitIndex % 64 的位设置为0。
    3. 调用 recalculateWordsInUse(),因为这个操作可能清除了最高的有效位。
  • public void flip(int bitIndex):

    BitSet.java

    java 复制代码
    public void flip(int bitIndex) {
        if (bitIndex < 0)
            throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex);
    
        int wordIndex = wordIndex(bitIndex);
        expandTo(wordIndex);
    
        words[wordIndex] ^= (1L << bitIndex); // 使用位异或操作翻转位
    
        recalculateWordsInUse(); // 翻转可能改变最高的置位
        checkInvariants();
    }

    翻转指定索引的位的值。

    1. 计算 wordIndex 并调用 expandTo(wordIndex)
    2. 通过位异或操作 ^= 翻转 words[wordIndex] 中对应 bitIndex % 64 的位。
    3. 调用 recalculateWordsInUse()
  • public boolean get(int bitIndex):

    java 复制代码
    public boolean get(int bitIndex) {
        if (bitIndex < 0)
            throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex);
    
        checkInvariants();
    
        int wordIndex = wordIndex(bitIndex);
        return (wordIndex < wordsInUse) // 必须在逻辑大小内
            && ((words[wordIndex] & (1L << bitIndex)) != 0); // 检查对应位是否为1
    }

    获取指定索引的位的值。

    1. 如果 wordIndex 超出 wordsInUse,则该位为 false
    2. 否则,通过位与操作 & 检查 words[wordIndex] 中对应 bitIndex % 64 的位是否为1。

范围操作 (例如 set(int fromIndex, int toIndex))

这类方法会处理跨越多个 long word 的情况。它们通常会:

  1. 计算起始 wordIndex (startWordIndex) 和结束 wordIndex (endWordIndex)。
  2. 调用 expandTo(endWordIndex) 确保容量。
  3. 分别处理三种情况:
    • 单个 word : startWordIndex == endWordIndex。操作只涉及一个 long。会创建一个掩码,只影响范围内的位。
    • 多个 words :
      • 第一个 word : 创建一个掩码,操作从 fromIndex % 64 到该 word 末尾的位。
      • 中间的 words : 如果有,这些 words 会被整个设置为目标值 (例如,WORD_MASK 表示全1,0 表示全0)。
      • 最后一个 word : 创建一个掩码,操作从该 word 开头到 (toIndex - 1) % 64 的位。

例如 set(int fromIndex, int toIndex):

BitSet.java

java 复制代码
// ...
        long firstWordMask = WORD_MASK << fromIndex; // 保留 fromIndex 右边的位 (包括 fromIndex)
        long lastWordMask  = WORD_MASK >>> -toIndex;  // 保留 toIndex 左边的位 (不包括 toIndex)
        if (startWordIndex == endWordIndex) {
            // Case 1: One word
            words[startWordIndex] |= (firstWordMask & lastWordMask);
        } else {
            // Case 2: Multiple words
            // Handle first word
            words[startWordIndex] |= firstWordMask;

            // Handle intermediate words, if any
            for (int i = startWordIndex+1; i < endWordIndex; i++)
                words[i] = WORD_MASK; // 全部设为1

            // Handle last word (restores invariants)
            words[endWordIndex] |= lastWordMask;
        }
// ...

WORD_MASK << fromIndex (等价于 WORD_MASK << (fromIndex % 64)) 会生成一个掩码,其中从 fromIndex % 64 位开始到最高位都是1,其余是0。

WORD_MASK >>> -toIndex (等价于 WORD_MASK >>> (64 - (toIndex % 64))toIndex % 64 != 0 时,或者 0toIndex % 64 == 0 时) 会生成一个掩码,其中从最低位到 (toIndex - 1) % 64 位是1,其余是0。 这两个掩码结合起来用于精确操作范围内的位。

转换方法

  • public static BitSet valueOf(long[] longs) / valueOf(ByteBuffer bb) / valueOf(byte[] bytes) : 这些静态工厂方法用于从 long 数组、ByteBufferbyte 数组创建 BitSet。它们通常处理小端字节序(LITTLE_ENDIAN)。valueOf(long[]) 会复制数组并去除末尾的0值word。valueOf(ByteBuffer)valueOf(byte[]) 会将字节数据转换为 long word。

  • public byte[] toByteArray() : 将 BitSet 转换为 byte 数组,同样采用小端字节序。它会计算实际需要的字节数,避免末尾不必要的0字节。

  • public long[] toLongArray() : 将 BitSet 的内部 words 数组(只包括 wordsInUse 部分)复制并返回。

valueOf(ByteBuffer bb) - 从 ByteBuffer 构建 BitSet

这个静态工厂方法允许你从一个 ByteBuffer 对象中创建 BitSet。这在需要从外部数据源(如文件、网络流)解析位集时非常有用。

实现分析:

java 复制代码
    public static BitSet valueOf(ByteBuffer bb) {
        // 1. 创建缓冲区的切片,并设置为小端字节序 (LITTLE_ENDIAN)
        //    BitSet 内部处理字节到 long 的转换时,通常遵循小端模式,
        //    即一个 long 中的最低有效字节先出现。
        bb = bb.slice().order(ByteOrder.LITTLE_ENDIAN);
        int n;
        // 2. 从后向前找到第一个非零的字节,确定实际有效的字节数 n。
        //    这是为了去除尾部的全零字节,优化存储。
        for (n = bb.remaining(); n > 0 && bb.get(n - 1) == 0; n--)
            ;
        // 3. 根据有效字节数 n 计算所需的 long 数组大小。
        //    每个 long 可以存 8 个字节 (64位)。
        long[] words = new long[(n + 7) / 8];
        bb.limit(n); // 限制 ByteBuffer 的读取范围到有效字节。
        int i = 0;
        // 4. 如果剩余字节数足够组成一个或多个完整的 long (8字节),则直接读取。
        while (bb.remaining() >= 8)
            words[i++] = bb.getLong();
        // 5. 处理剩余不足 8 字节的部分。
        //    逐个字节读取,并按照小端序组合成最后一个 long。
        for (int remaining = bb.remaining(), j = 0; j < remaining; j++)
            words[i] |= (bb.get() & 0xffL) << (8 * j);
        // 6. 使用构造的 long 数组创建新的 BitSet 实例。
        return new BitSet(words);
    }

有意思的点:

  • 字节序处理 : 明确指定 ByteOrder.LITTLE_ENDIAN,确保了跨平台和与其他系统交互时数据解释的一致性。
  • 效率优化 : 通过 bb.slice() 避免修改原始 buffer 状态,通过预先计算有效字节数 nbb.limit(n) 减少不必要的处理。
  • 分段处理 : 先批量读取完整的 long,再细致处理尾部不足一个 long 的字节,兼顾了效率和正确性。

toByteArray() - 将 BitSet 转换为 byte 数组

这个方法是 valueOf(byte[] bytes) 的逆操作,将 BitSet 的内容序列化为一个字节数组,同样采用小端字节序。

实现分析

BitSet.java

java 复制代码
    public byte[] toByteArray() {
        int n = wordsInUse;
        // 1. 如果 BitSet 为空,返回空字节数组。
        if (n == 0)
            return new byte[0];
        // 2. 计算输出字节数组的长度。
        //    首先假设除了最后一个 word 外,其他 word 都占满 8 字节。
        int len = 8 * (n - 1);
        //    然后计算最后一个 word (words[n-1]) 实际占用的字节数。
        //    通过不断右移8位并检查是否为0来确定。
        for (long x = words[n - 1]; x != 0; x >>>= 8)
            len++;
        byte[] bytes = new byte[len];
        // 3. 使用 ByteBuffer 辅助写入,并设置为小端字节序。
        ByteBuffer bb = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
        // 4. 写入 n-1 个完整的 long。
        for (int i = 0; i < n - 1; i++)
            bb.putLong(words[i]);
        // 5. 逐字节写入最后一个 long (words[n-1]) 的有效部分。
        for (long x = words[n - 1]; x != 0; x >>>= 8)
            bb.put((byte) (x & 0xff));
        return bytes;
    }

有意思的点:

  • 精确长度计算 : 准确计算输出字节数组的长度,避免浪费空间,特别是对于最后一个 long 中未完全使用的部分。
  • ByteBuffer 的使用 : 再次利用 ByteBuffer 及其字节序设置,简化了 long 到字节序列的转换。

nextSetBit(int fromIndex) -查找下一个为 true 的位

这个方法从指定的索引 fromIndex (包含) 开始,查找并返回第一个值为 true 的位的索引。如果找不到,则返回 -1。这对于遍历 BitSet 中所有设置的位非常有用。

实现分析:

java 复制代码
    public int nextSetBit(int fromIndex) {
        if (fromIndex < 0)
            throw new IndexOutOfBoundsException("fromIndex < 0: " + fromIndex);

        checkInvariants(); // 内部一致性检查

        // 1. 计算 fromIndex 所在的 word 的索引 u。
        int u = wordIndex(fromIndex);
        // 2. 如果 u 超出了实际使用的 word 范围,说明后面没有置位的 bit 了。
        if (u >= wordsInUse)
            return -1;

        // 3. 处理第一个 word (words[u])。
        //    先获取 words[u] 的内容,然后清除掉 fromIndex % 64 之前的所有位。
        //    例如,如果 fromIndex 是 65 (在第1个word的第1个bit),
        //    那么 fromIndex % 64 是 1。 (1L << fromIndex) 会产生一个掩码,
        //    这里实际上是想从 fromIndex 指定的 bit 开始检查。
        //    word = words[u] & (WORD_MASK << fromIndex) 的含义是:
        //    WORD_MASK 是全1 (0xff...ffL)。
        //    (WORD_MASK << fromIndex) 实际上是 (WORD_MASK << (fromIndex % 64))
        //    它会生成一个掩码,其中 fromIndex % 64 位以及其后的位为1,之前的位为0。
        //    所以 word 中只保留了从 fromIndex 开始的那些位。
        long word = words[u] & (WORD_MASK << fromIndex);

        while (true) {
            // 4. 如果当前处理的 word 不为0,说明在这个 word 中找到了置位的 bit。
            if (word != 0)
                // Long.numberOfTrailingZeros(word) 返回 word 中最低位1后面的0的个数。
                // (u * BITS_PER_WORD) 是当前 word 的起始 bit 索引。
                // 两者相加即为全局的 bit 索引。
                return (u * BITS_PER_WORD) + Long.numberOfTrailingZeros(word);
            
            // 5. 如果当前 word 为0,或者处理完第一个 word 后,移动到下一个 word。
            u++;
            // 6. 如果已经检查完所有使用的 word,则返回 -1。
            if (u == wordsInUse)
                return -1;
            // 7. 获取下一个 word 的内容。
            word = words[u];
        }
    }

有意思的点:

  • 位操作的巧妙运用 : (WORD_MASK << fromIndex) (应理解为 WORD_MASK << (fromIndex % ADDRESS_BITS_PER_WORD)) 用于快速屏蔽掉一个 long 中不需要检查的前缀位。
  • Long.numberOfTrailingZeros() : 高效地找到一个 long 中最低位的 1。这是 HotSpot VM 的一个 intrinsic 方法,性能很高。
  • 分段查找 : 先在起始 word 内查找,然后逐个 word 查找,逻辑清晰。

and(BitSet set) - 执行逻辑与操作

这个方法对当前的 BitSet 和传入的 BitSet 执行按位与(AND)操作,结果存储在当前的 BitSet 中。

实现分析

BitSet.java

java 复制代码
    public void and(BitSet set) {
        // 1. 如果参数是自身,无需操作。
        if (this == set)
            return;

        // 2. 清理 words 数组,确保 wordsInUse 是准确的。
        //    如果 wordsInUse 比实际的 words 数组长度小,
        //    且 wordsInUse 位置的 word 为0,则 wordsInUse 可能不是最新的。
        //    这里通过将 this.wordsInUse 之后、set.wordsInUse 之前的 words[i] 清零,
        //    确保后续的 recalculateWordsInUse 能正确工作。
        while (wordsInUse > set.wordsInUse)
            words[--wordsInUse] = 0;

        // 3. 遍历两个 BitSet 中 words 数组的共同部分,执行按位与。
        for (int i = 0; i < wordsInUse; i++)
            words[i] &= set.words[i];

        // 4. 重新计算当前 BitSet 的 wordsInUse,因为某些 word 可能在 AND 操作后变为0。
        recalculateWordsInUse();
        checkInvariants(); // 内部一致性检查
    }

有意思的点:

  • 就地修改 : 操作结果直接修改当前 BitSet 实例。
  • 长度对齐 : 操作前,通过 while (wordsInUse > set.wordsInUse) 将当前 BitSet 的逻辑长度调整得不超过参数 BitSet 的逻辑长度(对于 AND 操作,超出部分的结果必然是0)。
  • recalculateWordsInUse() : 在位操作后,wordsInUse 可能需要更新,因为一些高位的 long 可能变成了0。这个辅助方法会从后向前检查 words 数组,找到最后一个非零的 long 来确定新的 wordsInUse

此外还有 or,xor,这两个方法基本类似,但是对于不同长度还需要设置剩余长度的位。

cardinality() - 计算置位(为 true)的位数

这个方法返回 BitSet 中值为 true 的位的总数。

实现分析

BitSet.java

java 复制代码
    public int cardinality() {
        int sum = 0;
        // 遍历所有实际在使用的 words
        for (int i = 0; i < wordsInUse; i++)
            // Long.bitCount(long i) 是一个高效的内置方法,
            // 用于计算一个 long 值中二进制表示下 '1' 的个数。
            sum += Long.bitCount(words[i]);
        return sum;
    }

有意思的点:

  • Long.bitCount() : 再次利用了 Long 类的内置高效方法。这个方法通常由 JVM intrinsics 实现,速度非常快,远比自己循环判断每一位要高效。

get(int fromIndex, int toIndex) - 获取指定范围的子 BitSet

这个方法创建一个新的 BitSet,其内容是当前 BitSetfromIndex (包含) 到 toIndex (不包含) 范围内的位。

实现分析

BitSet.java

java 复制代码
    public BitSet get(int fromIndex, int toIndex) {
        checkRange(fromIndex, toIndex); // 检查范围有效性
        checkInvariants(); // 内部一致性检查

        int len = length(); // 获取当前 BitSet 的逻辑长度

        // 1. 如果指定范围没有置位,或者范围为空,返回一个空的 BitSet。
        if (len <= fromIndex || fromIndex == toIndex)
            return new BitSet(0);

        // 2. 优化:如果 toIndex 超出实际长度,将其修正为实际长度。
        if (toIndex > len)
            toIndex = len;

        // 3. 创建结果 BitSet,大小为 toIndex - fromIndex。
        BitSet result = new BitSet(toIndex - fromIndex);
        // 4. 计算结果 BitSet 需要的 word 数量。
        int targetWords = wordIndex(toIndex - fromIndex - 1) + 1;
        // 5. 计算源 BitSet 开始读取的 word 索引。
        int sourceIndex = wordIndex(fromIndex);
        // 6. 判断源范围的起始位是否是 word 对齐的 (即 fromIndex % 64 == 0)。
        boolean wordAligned = ((fromIndex & BIT_INDEX_MASK) == 0);

        // 7. 处理所有完整的 word(除了可能跨越边界的最后一个 word)。
        for (int i = 0; i < targetWords - 1; i++, sourceIndex++)
            result.words[i] = wordAligned ? words[sourceIndex] :
                // 如果未对齐,需要从 sourceIndex 和 sourceIndex+1 两个 word 中拼接。
                // (words[sourceIndex] >>> fromIndex) 获取 sourceIndex 中 relevant部分
                // (words[sourceIndex+1] << -fromIndex) 获取 sourceIndex+1 中 relevant部分
                // (注意: -fromIndex 在这里实际上是 64 - (fromIndex % 64))
                (words[sourceIndex] >>> fromIndex) |
                (words[sourceIndex+1] << -fromIndex);

        // 8. 处理最后一个 word。
        //    lastWordMask 用于屏蔽掉 toIndex 之后的不相关位。
        long lastWordMask = WORD_MASK >>> -toIndex; // (注意: -toIndex 实际上是 64 - (toIndex % 64))
        result.words[targetWords - 1] =
            // 判断目标范围的结束位在源 word 内的偏移是否小于起始位在源 word 内的偏移
            // 如果是,说明最后一个目标 word 跨越了两个源 word。
            ((toIndex-1) & BIT_INDEX_MASK) < (fromIndex & BIT_INDEX_MASK)
            ? /* straddles source words */
            ((words[sourceIndex] >>> fromIndex) |
             (words[sourceIndex+1] & lastWordMask) << -fromIndex)
            : /* within one source word */
            ((words[sourceIndex] & lastWordMask) >>> fromIndex);

        // 9. 设置结果 BitSet 的 wordsInUse 并重新计算。
        result.wordsInUse = targetWords;
        result.recalculateWordsInUse();
        result.checkInvariants();

        return result;
    }

有意思的点:

  • 范围和对齐处理 : 这是 BitSet 中最复杂的操作之一。它需要精确处理源范围的起始和结束位可能不在 long (word) 边界上的情况。
  • 位移操作的精妙 : 大量的位移操作 (>>>, <<) 被用来从源 words 中提取、组合所需的位到目标 words
    • words[sourceIndex] >>> fromIndex:将源 word 右移 fromIndex % 64 位,使得需要的位移动到低位。
    • words[sourceIndex+1] << -fromIndex (等效于 words[sourceIndex+1] << (64 - (fromIndex % 64))):将下一个源 word 的低位左移,以补充上一个 word 右移后空出来的高位。
  • 条件化的拼接逻辑 : 根据源范围是否对齐以及最后一个目标 word 是否跨越源 word 边界,有不同的处理逻辑。

总结

BitSet 不是 线程安全的。如果多个线程并发修改一个 BitSet 实例而没有外部同步,可能会导致不确定的行为。

BitSet 通过使用 long 数组作为位存储的底层结构,实现了高效的空间利用和快速的位操作。它通过 wordsInUse 字段动态管理逻辑大小,并通过 ensureCapacityexpandTo 方法按需扩展物理存储。核心的位操作(set, clear, flip, get)都依赖于高效的位运算。范围操作则通过对起始、中间和结束 word 的分别处理来实现。

相关推荐
mashanshui1 小时前
Https之(二)TLS的DH密钥协商算法
算法·https·tls·dh·ecdhe
admiraldeworm3 小时前
Spring Boot + Spring AI 最小可运行 Demo
java·人工智能·ai
chenglin0163 小时前
ES_数据存储知识
java·服务器·elasticsearch
wearegogog1234 小时前
MATLAB的脉搏信号分析预处理
算法·matlab
fs哆哆4 小时前
在VB.net中一维数组,与VBA有什么区别
java·开发语言·数据结构·算法·.net
蝎子莱莱爱打怪4 小时前
Hadoop3.3.5、Hbase2.6.1 集群搭建&Phoenix使用记录
大数据·后端·hbase
johnZhangqi4 小时前
深圳大学-计算机信息管理课程实验 C++ 自考模拟题
java·开发语言·c++
wjt1020204 小时前
机器学习--续
算法·机器学习
David爱编程4 小时前
并发编程三大特性全解析:原子性、可见性、有序性,一文讲透!
java·后端
Sally璐璐4 小时前
Go语言变量声明与初始化详解
java·开发语言·golang