BitSet

BitSet

一、BitSet简介

​ 类实现了一个按需增长的位向量。位 set 的每个组件都有一个boolean值。用非负的整数将BitSet的位编入索引。可以对每个编入索引的位进行测试、设置或者清除。通过逻辑与、逻辑或和逻辑异或操作,可以使用一个BitSet修改另一个BitSet的内容。

​ 默认情况下,set 中所有位的初始值都是false。

​ 每个位 set 都有一个当前大小,也就是该位 set 当前所用空间的位数。注意,这个大小与位 set 的实现有关,所以它可能随实现的不同而更改。位 set 的长度与位 set 的逻辑长度有关,并且是与实现无关而定义的。

​ 除非另行说明,否则将 null 参数传递给BitSet中的任何方法都将导致NullPointerException。

​ 在没有外部同步的情况下,多个线程操作一个BitSet是不安全的

二、基本原理

内部维护一个long数组words, 每一个bit位值为0或1即false和true, 初始只有一个long,所以BitSet最小的size是64,当随着存储的元素越来越多,BitSet内部会动态扩充(每次扩容为原来的两倍),最终内部是由N个long来存储;

用1位来表示一个数据是否出现过,0为没有出现过,1表示出现过。使用用的时候既可根据某一个是否为0表示,此数是否出现过。一个1G的空间,有8*1024*1024*1024=8.58*10^9bit,也就是可以表示85亿个不同的数。

三、Java中BitSet的实现

BitSet位于java.util这个包, 以下基于jdk8对BitSet实现进行分析;

3.1 部分属性

  • ADDRESS_BITS_PER_WORD: 值为6, 一个long类型占8个byte, 有64bit, 而 2^6 = 64;
  • BITS_PER_WORD: 值为1 << ADDRESS_BITS_PER_WORD = 64;
  • BIT_INDEX_MASK: 索引掩码,
  • words: BitSet内部维护的long类型数组, BitSet的核心;
  • wordsInUse: 记录BitSet中words使用了的长度;
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 final static int ADDRESS_BITS_PER_WORD = 6;
   private final static int BITS_PER_WORD = 1 << ADDRESS_BITS_PER_WORD;
   private final static int BIT_INDEX_MASK = BITS_PER_WORD - 1;

   /* Used to shift left or right for a partial word mask */
   private static final long WORD_MASK = 0xffffffffffffffffL;

   /**
    * The internal field corresponding to the serialField "bits".
    */
   private long[] words;
   /**
    * The number of words in the logical size of this BitSet.
    */
   private transient int wordsInUse = 0;
}

3.2 常用方法

BitSet主要分析的方法:

  • 构造方法
  • 反转某一位: flip
  • 设置某一指定位: set
  • 获取某一位: get
  • 清空bit位: clear
  • 获取size, length: size, length
  • BitSet存储的数量: cardinality
  • 下一个bit位: nextSetBit

1. 构造方法

  • 无参构造, 默认words长度为1
public BitSet() {
    initWords(BITS_PER_WORD); // 初始化words
    sizeIsSticky = false;
}
  • 有参构造, 根据nbits计算出words长度
public BitSet(int nbits) {
    // nbits can't be negative; size 0 is OK
    if (nbits < 0)
        throw new NegativeArraySizeException("nbits < 0: " + nbits);
    initWords(nbits);
    sizeIsSticky = true;
}
  • 初始化words;
// 根据nbits大小初始化 words的长度
private void initWords(int nbits) {
    words = new long[wordIndex(nbits-1) + 1];
}
// 将bitIndex右移64位, 计算出bitIndex在words中的索引;
private static int wordIndex(int bitIndex) {
    return bitIndex >> ADDRESS_BITS_PER_WORD;
}

2. flip反转某一位

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();
}

3. 一个检查扩容;两个检查函数

  • 检查扩容
// wordsInUse表示BitSet中使用的
private void expandTo(int wordIndex) {
    int wordsRequired = wordIndex+1;
    if (wordsInUse < wordsRequired) {
        ensureCapacity(wordsRequired);
        wordsInUse = wordsRequired;
    }
}
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;
    }
}
  • 更新wordsInUse的值, wordsInUse的值可以被用来计算length;
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
}
  • checkInvariants 可以看出是检查内部状态,尤其是wordsInUse是否合法的函数。
private void checkInvariants() {
    assert(wordsInUse == 0 || words[wordsInUse - 1] != 0);
    assert(wordsInUse >= 0 && wordsInUse <= words.length);
    assert(wordsInUse == words.length || words[wordsInUse] == 0);
}

4. set

注意: 按位左移 若移动的位数超过被移动数类型的bit数, 则会对移位数取模, 再左移; 例如 11L<<65 等价于 11L << 1

public void set(int bitIndex) {
    if (bitIndex < 0)
        throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex);

    int wordIndex = wordIndex(bitIndex);
    expandTo(wordIndex);

    words[wordIndex] |= (1L << bitIndex); // Restores invariants

    checkInvariants();
}

5. get

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

6. 清空BitSet

  • 清空所有的bit位,即全部置0。通过循环方式来以此以此置0。
public void clear() {  
    while (wordsInUse > 0)  
        words[--wordsInUse] = 0;  
}  
  • 清空某一位
public void clear(int bitIndex) {
    if (bitIndex < 0)
        throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex);

    int wordIndex = wordIndex(bitIndex);
    if (wordIndex >= wordsInUse)
        return;

    words[wordIndex] &= ~(1L << bitIndex);

    recalculateWordsInUse();
    checkInvariants();
}

第一行是参数检查,如果bitIndex小于0,则抛参数非法异常。后面执行的是bitset中操作中经典的两步:a. 找到对应的long b. 操作对应的位。
a. 找到对应的long。 这行语句是 int wordIndex = wordIndex(bitIndex);
b. 操作对应的位。常见的位操作是通过与特定的mask进行逻辑运算来实现的。因此,首先获取 mask(掩码)。
对于 clear某一位来说,它需要的掩码是指定位为0,其余位为1,然后与对应的long进行&运算。
~(1L << bitIndex); 即获取mask
words[wordIndex] &= ; 执行相应的运算。
注意:这里的参数检查,对负数index跑出异常,对超出大小的index,不做任何操作,直接返回。

  • 清空指定范围的那些bits
 public void clear(int fromIndex, int toIndex) {
    checkRange(fromIndex, toIndex);

    if (fromIndex == toIndex)
        return;

    int startWordIndex = wordIndex(fromIndex);
    if (startWordIndex >= wordsInUse)
        return;

    int endWordIndex = wordIndex(toIndex - 1);
    if (endWordIndex >= wordsInUse) {
        toIndex = length();
        endWordIndex = wordsInUse - 1;
    }

    long firstWordMask = WORD_MASK << fromIndex;
    long lastWordMask  = WORD_MASK >>> -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] = 0;

        // Handle last word
        words[endWordIndex] &= ~lastWordMask;
    }

    recalculateWordsInUse();
    checkInvariants();
}

方法是将这个范围分成三块,startword; interval words; stopword。
其中startword,只要将从start位到该word结束位全部置0;interval words则是这些long的所有bits全部置0;而stopword这是从起始位置到指定的结束位全部置0。
而特殊情形则是没有startword和stopword是同一个long。
具体的实现,参照代码,是分别作出两个mask,对startword和stopword进行操作。

7. size, length

  • 获取size, size即为words数组中bit位的数量
public int size() {
    return words.length * BITS_PER_WORD; // BITS_PER_WORD = 64
}
  • length, 值等于使用了的最高的bit位索引 + 1;
public int length() {
    if (wordsInUse == 0)
        return 0;

    return BITS_PER_WORD * (wordsInUse - 1) +
        (BITS_PER_WORD - Long.numberOfLeadingZeros(words[wordsInUse - 1]));
}

8. cardinality, BitSet中存储的数据个数

  • Long.bitCount(words[i]): 计算出一个long类型的二进制中1的个数;
public int cardinality() {
    int sum = 0;
    for (int i = 0; i < wordsInUse; i++)
        sum += Long.bitCount(words[i]);
    return sum;
}

9. nextSetBit

/**
 * fromIndex后面为bit为值为1的bit索引
 * @param  fromIndex [description]
 * @return           [description]
 */
public int nextSetBit(int fromIndex) {
    if (fromIndex < 0)
        throw new IndexOutOfBoundsException("fromIndex < 0: " + fromIndex);

    checkInvariants();

    int u = wordIndex(fromIndex);
    if (u >= wordsInUse)
        return -1;

    long word = words[u] & (WORD_MASK << fromIndex);

    while (true) {
        if (word != 0)
            return (u * BITS_PER_WORD) + Long.numberOfTrailingZeros(word);
        if (++u == wordsInUse)
            return -1;
        word = words[u];
    }

四、Java中Bitset的使用

由于BitSet中可以实现1G的内存存储8亿个不同的数据, 因此可以用来处理大量数据的情况;

使用场景:

  • 对海量数据进行一些统计工作的时候,比如日志分析、用户数统计

如统计40亿个数据中没有出现的数据,将40亿个不同数据进行排序等。
现在有1千万个随机数,随机数的范围在1到1亿之间。现在要求写出一种算法,将1到1亿之间没有在随机数中的数求出来

  • mq中, 防止消息被重复消费;
package com.ricky.java.test;
 
import java.util.BitSet;
 
public class BitSetTest {
 
	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// 字符串去除重复出现的字符
		containChars("abcdfab");
		// 对数组数据进行排序, 去重;
		sortArray(new int[] { 423, 700, 9999, 2323, 356, 6400, 1,2,3,2,2,2,2 });
	}
	
	/**
	 * 字符串去除重复出现的字符
	 * @param str [description]
	 */
    public static void containChars(String str) {
        BitSet used = new BitSet();
        for (int i = 0; i < str.length(); i++)
            used.set(str.charAt(i)); // set bit for char
 
        StringBuilder sb = new StringBuilder();
        sb.append("[");
        int size = used.size();
        System.out.println(size);
        for (int i = 0; i < size; i++) {
            if (used.get(i)) {
                sb.append((char) i);
            }
        }
        sb.append("]");
        System.out.println(sb.toString());
    }
    
    /**
     * 对数组去重排序
     */
    public static void sortArray(int[] array) {
        
        BitSet bitSet = new BitSet(2 << 13);
        // 虽然可以自动扩容,但尽量在构造时指定估算大小,默认为64
        System.out.println("BitSet size: " + bitSet.size());
 
        for (int i = 0; i < array.length; i++) {
            bitSet.set(array[i]);
        }
        //剔除重复数字后的元素个数
        int bitLen=bitSet.cardinality();    
 
        //进行排序,即把bit为true的元素复制到另一个数组
        int[] orderedArray = new int[bitLen];
        int k = 0;
        for (int i = bitSet.nextSetBit(0); i >= 0; i = bitSet.nextSetBit(i + 1)) {
            orderedArray[k++] = i;
        }
 
        System.out.println("After ordering: ");
        for (int i = 0; i < bitLen; i++) {
            System.out.print(orderedArray[i] + "\t");
        }
         
        System.out.println("iterate over the true bits in a BitSet");
        //或直接迭代BitSet中bit为true的元素iterate over the true bits in a BitSet
        for (int i = bitSet.nextSetBit(0); i >= 0; i = bitSet.nextSetBit(i + 1)) {
            System.out.print(i+"\t");
        }
    }
}
posted @ 2018-08-15 01:45  阔乐  阅读(339)  评论(0编辑  收藏  举报