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 的分别处理来实现。

相关推荐
程序员爱钓鱼3 分钟前
Go语言实战案例-简易计算器(加减乘除)
后端
DoraBigHead6 分钟前
小哆啦解题记——两数失踪事件
前端·算法·面试
不太可爱的大白7 分钟前
Mysql分片:一致性哈希算法
数据库·mysql·算法·哈希算法
学不会就看8 分钟前
Django--01基本请求与响应流程
后端·python·django
AI+程序员在路上12 分钟前
Qt6中模态与非模态对话框区别
开发语言·c++·qt
Tiandaren4 小时前
Selenium 4 教程:自动化 WebDriver 管理与 Cookie 提取 || 用于解决chromedriver版本不匹配问题
selenium·测试工具·算法·自动化
胚芽鞘6814 小时前
关于java项目中maven的理解
java·数据库·maven
nbsaas-boot5 小时前
Java 正则表达式白皮书:语法详解、工程实践与常用表达式库
开发语言·python·mysql
岁忧5 小时前
(LeetCode 面试经典 150 题 ) 11. 盛最多水的容器 (贪心+双指针)
java·c++·算法·leetcode·面试·go
chao_7895 小时前
二分查找篇——搜索旋转排序数组【LeetCode】两次二分查找
开发语言·数据结构·python·算法·leetcode