第一部分: 引用自 https://yq.aliyun.com/articles/136154
《java collection》学习
简介
JAVA中BitSet就是“位图”数据结构,根据“位图”的语义,数据的存在性可以使用bit位上的1或0来表示;一个bit具有2个值:0和1,正好可以用来表示false和true。
对于判断“数据是否存在”的场景,我们通常使用HashMap来存储,不过hashmap这个数据结构KEY和Value的保存需要消耗较多的内存,不适合保存较多的数据,即大数据场景;
比如在有10亿条URL中判定一个www.baidu.com/a
是否存在?
- a :如果我们使用常规的hashmap来保存将是不现实的,因为URL本身需要占据较多的内存而无法直接操作。
- b : 如果我们使用bitset来保存,那么可以对一条URL求
hashcode
,并将数字映射在bitset上,那么事实上它只需要bitset上的一个bit位即可,即我们1位空间即可表达一个URL字符串的存在性。
BitSet原理
JAVA中,一个long型数字占用64
位空间,根据上述“位图”的概念,那么一个long型数字就可以保存64个数字的“存在性”状态(无碰撞冲突时)。
比如50个数字{0,1,10,…63},判定“15”是否存在,
- 如果是用数组或者hashmap保存,然后再去判定,那么保存这些这些数据需要占用64 * 64位;
- 如果使用位图,那么一个long型数字即可。
(如果换成50个字符串,那么其节约的空间可能更大)。
- BitSet只
面向数字
比较,如果像使用BitSet,那么可以将其hashcode
值映射在bitset中。 - 首先我们需要知道:
1,1<<64,1<<128,1<<192...等
,这些数字的计算结果是相等的(位运算);
这也是一个long数字,只能表达连续的(或者无冲突的)64个数字的状态,即如果把数字1在long中用位表示,那么数字64将无法通过同一个long数字中位表示–冲突;
BitSet内部,是一个long[]数组
,数组的大小
由bitSet接收的最大数字决定
.这个数组将数字分段表示[0,63],[64,127],[128,191]…。
|------------|----------|----------|----------|----------|
|
| 数字范围 [0,63] [64,127] [128,191] ... |
|------------|----------|----------|----------|----------|
|
|long数组索引 0 1 2 ... |
|------------|----------|----------|----------|----------|
- bitSet内部的long[]数组是基于向量的,即随着接收的最大数字而动态扩展。
BitSet与Hashcode冲突
因为BitSet API只能接收int型的数字,即只能判定int数字是否在bitSet中存在。
所以,对于String类型,我们通常使用它的hashcode,但这有一种隐患,java中hashcode存在冲突问题
,即不同的String可能得到的hashcode是一样的(即使不重写hashcode方法).
如果我们不能很好的解决这个问题,那么就会出现“数据抖动”
—不同的hashcode算法、运行环境、bitSet容量,会导致判断的结果有所不同。
比如A、B两个字符串,它们的hashcode一样,
如果A在BitSet中“着色”(值为true),那么检测B是否在BitSet存在时,也会得到true。
这个问题该如何解决或者缓解呢?
我们可以对一个String使用多个hashcode算法
,生成多个hashcode
,
然后在同一个BitSet进行多次“着色”,在判断存在性时,只有所有的着色位为true时,才判定成功。
其实我们能够看出,这种方式降低了误判的概率。但是如果BitSet中存储了较多的数字,那么互相覆盖着色,最终数据冲突的可能性会逐渐增加,最终仍然有一定概率的判断失误。
所以在hashcode算法的个数与实际String的个数之间有一个权衡,我们建议:
“hashcode算法个数 * String字符串的个数” < Integer.MAX_VALUE * 0.8
BloomFilter(布隆姆过滤器)
BloomFilter 的设计思想和BitSet有较大的相似性,目的也一致,它的核心思想也是使用多个Hash算法在一个“位图”结构上着色,最终提高“存在性”判断的效率。
结论
BitSet同BloomFilter一样:
- 如果判定结果为false,那么数据一定是不存在;
- 但是如果结果为true,可能数据存在,也可能不存在(冲突覆盖)
BitSet源码分析
注意:BitSet 并没有实现
Set
接口
构造函数
public class BitSet implements Cloneable, java.io.Serializable {
//BitSets被打包成“words”数组
//每个“word”表示一个long型数字,java中一个long占64bit, 2^6
private final static int ADDRESS_BITS_PER_WORD = 6;
private final static int BITS_PER_WORD = 1 << ADDRESS_BITS_PER_WORD; // 64
private final static int BIT_INDEX_MASK = BITS_PER_WORD - 1; // 63
private static final long WORD_MASK = 0xffffffffffffffffL;
private long[] words;
private transient int wordsInUse = 0;
//"words"大小是否由用户指定
//如果是用户指定的,这说明用户知道他即将要做什么,程序会努力保护BitSet的大小不变;
private transient boolean sizeIsSticky = false;
//无参的构造函数
public BitSet() {
initWords(BITS_PER_WORD);
sizeIsSticky = false; //false
}
public BitSet(int nbits) {
// nbits可以为0,但是不能为负数
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];
}
//计算数组的长度,由于一个long占用64位
//最终数组长度为 bitIndex / 64 == bitIndex >> 6
private static int wordIndex(int bitIndex) {
return bitIndex >> ADDRESS_BITS_PER_WORD;
}
}
set(),clear()
将第bitIndex
数设置为true
.
public void set(int bitIndex) {
if (bitIndex < 0)
throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex);
//计算word所在数组的index:
int wordIndex = wordIndex(bitIndex);
expandTo(wordIndex);
// 1L == 1L << 64 == 1L << 128 ...
words[wordIndex] |= (1L << bitIndex);
checkInvariants();
}
private void expandTo(int wordIndex) {
int wordsRequired = wordIndex+1;
if (wordsInUse < wordsRequired) {
//数组扩容
ensureCapacity(wordsRequired);
wordsInUse = wordsRequired;
}
}
public void set(int bitIndex, boolean value) {
if (value)
set(bitIndex);
else
clear(bitIndex);
}
public void clear(int bitIndex) {
if (bitIndex < 0)
throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex);
int wordIndex = wordIndex(bitIndex);
if (wordIndex >= wordsInUse)
return;
//~(1L << bitIndex) 取反码
words[wordIndex] &= ~(1L << bitIndex);
recalculateWordsInUse();
checkInvariants();
}
//按照区间设置
public void set(int fromIndex, int toIndex);
public void set(int fromIndex, int toIndex, boolean value);
get()
返回boolean,表示指定值是否被“着色”
public boolean get(int bitIndex) {
if (bitIndex < 0)
throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex);
checkInvariants();
//定位到数组index
int wordIndex = wordIndex(bitIndex);
return (wordIndex < wordsInUse)
&& ((words[wordIndex] & (1L << bitIndex)) != 0);
}
get,set例
###step1,set(65)
set(65),wordIndex == 1, 1L << 65 == 2L;
words[1] = 0000000000000000000000000000000000000000000000000000000000000010 (2L)
###step2,get(65)
get(65), wordIndex == 1, 1L << 65 == 2L;
0000000000000000000000000000000000000000000000000000000000000010(2L)
&
0000000000000000000000000000000000000000000000000000000000000010(2L)
--------------------------------------------------------------------
2 != 0 == true
###step3,get(66)
get(66), wordIndex == 1, 1L << 66 == 4L;
0000000000000000000000000000000000000000000000000000000000000010(2L)
&
0000000000000000000000000000000000000000000000000000000000000100(4L)
--------------------------------------------------------------------
0 !=0 == false
逻辑运算
public void and(BitSet set) {
this[index] &= set[index];
}
public void or(BitSet set){
this[index] |= set[index];
}
//异或
public void xor(BitSet set){
this[index] ^= set[index]
}
//先给set取非,然后&
public void andNot(BitSet set){
this[index] &= ~set[index]
}
逻辑运算是计算是以数组内
“word”
为单位计算的,
如this[0] &= set[0],是比较【0,63】这个区间内所有的数字
作and()操作。
假设原始set{1, 2, 3, 4, 5}
和set{1, 3, 5, 7}.
size(),length()
public int size() {
//数组长度*64
return words.length * BITS_PER_WORD;
}
//返回最大的非0值所在位置;
public int length() {
if (wordsInUse == 0)
return 0;
// 64 * wordsInUse - 最大数字的高位0的个数
return BITS_PER_WORD * (wordsInUse - 1) +
(BITS_PER_WORD - Long.numberOfLeadingZeros(words[wordsInUse - 1]));
}