在 Java 中,官方提供了一个 Bitmap 的简单实现,它就是 java.util.BitSet
。
构造函数 在 BitSet 中,使用一个 long 类型的数组作为 Bitmap 的数据结构。我们首先查看 BitSet 的两个构造函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 private final static int ADDRESS_BITS_PER_WORD = 6 ;private final static int BITS_PER_WORD = 1 << ADDRESS_BITS_PER_WORD;private long [] words;public BitSet () { initWords(BITS_PER_WORD); sizeIsSticky = false ; } public BitSet (int nbits) { if (nbits < 0 ) throw new NegativeArraySizeException ("nbits < 0: " + nbits); initWords(nbits); sizeIsSticky = true ; } private void initWords (int nbits) { words = new long [wordIndex(nbits-1 ) + 1 ]; } private static int wordIndex (int bitIndex) { return bitIndex >> ADDRESS_BITS_PER_WORD; }
第一个构造函数默认会初始化一个长度为 1 的 long 型数组。第二个构造函数会根据指定的长度进行初始化。我们知道,Java 是没有 bit 这个基本类型的,而 long 类型的长度为 64 bit,那么如何将指定长度的 bit 数组转化为 long 类型的数组呢?答案就是将指定的长度除以 64,就可以得到 long 型数组的长度,即代码中的 bitIndex/64
,换成位运算就是 bitIndex >> 6
。
set 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 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(); } 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) { int request = Math.max(2 * words.length, wordsRequired); words = Arrays.copyOf(words, request); sizeIsSticky = false ; } }
BitSet 通过 set 方法放入元素。首先根据元素值计算该元素应该放在哪个 long 型数组元素上,即计算 wordIndex。然后根据 wordIndex 判断 Bitmap 是否需要扩容,如果需要扩容,则扩容为需要的长度的两倍。接下来的这段代码 words[wordIndex] |= (1L << bitIndex);
比较经典,也不好理解,我们可以先传入几个值来观察结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 System.out.println(1 <<0 ); System.out.println(1 <<1 ); System.out.println(1 <<2 ); System.out.println(1 <<3 ); System.out.println(1 <<4 ); System.out.println(1 <<5 ); System.out.println(1 <<6 ); System.out.println(1 <<7 ); 1 2 4 8 16 32 64 128
左移运算,相当于 1 * 2^bitIndex
,我们把它们转换成二进制的形式:
1 2 3 4 5 6 7 8 00000001 00000010 00000100 00001000 00010000 00100000 01000000 10000000
可以看到,每个 bitIndex 的值都被转换成了对应的 bit 位表示。BitSet 正是通过这种方式,将所有的整数值对应的位设置为 1。接下来就是将当前计算得出的值与原先数组元素的值使用按位或来进行合并,即:words[wordIndex] |= (1L << bitIndex);
。
get 1 2 3 4 5 6 7 8 9 10 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 ); }
get 方法用来判断一个元素是否在 BitSet 中。首先同样根据元素值计算该元素应该放在哪个 long 型数组元素上,即计算 wordIndex。如果已经使用的 long 型数组长度小于或等于该元素需要放置的数组索引值,则表示该元素一定不在 BitSet 中。接下来的 1L << bitIndex
可以计算出该元素在哪个 bit 位上,然后同 long 型数组对应的元素进行与运算,如果结果不等于 0,说明该元素存在。举个例子,假设要查找的元素为 5:
1 2 3 4 5 1L << bitIndex 00100000 // 如果已经存在元素 5 words[wordIndex] 00100000 // 如果并不存在元素 5(这里存在其它元素:0 1 2 3 4) words[wordIndex] 00011111
可以看到,原数组元素上是否存在其它元素并不影响该元素本身的判断,因为 1L << bitIndex
只在指定位置上为 1,其它位置均为 0,进行与运算后,其它位置的结果同样是 0。
参考
Java 的 BitSet 原理及应用
漫画:什么是Bitmap算法?