接着上一篇Blog:一道面试题与Java位操作 和 BitSet 库的使用,分析下Java源码中BitSet类的源码。

  

  位图(Bitmap),即位(Bit)的集合,是一种常用的数据结构,可用于记录大量的0-1状态,在很多地方都会用到,比如Linux内核(如inode,磁盘块)、Bloom Filter算法等,其优势是可以在一个非常高的空间利用率下保存大量0-1状态。在Java中,直接面向程序员的最小数据操作粒度是byte,并不提供能够直接操作某个bit的途径,但是程序员可以通过使用位运算符(& | ~ << >> 等等)自己封装位操作。如果不想自己动手,可以使用Java内置的BitSet类,其实现了位图数据结构并提供了一系列有用的接口。

  java.util.BitSet这个类不大,代码不到1200行,理解起来也不困难,下面分析一下关键的几处代码。(注意下面的代码是基于Oracle jdk1.7.0_45,或者点击这里看源码):

1.一些属性

     /*
* 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;

  其实注释已经写得很清楚,BitSet是用long[]来存储数据,一个long是64个bit,所以ADDRESS_BITS_PER_WORD就是6(2^6=64,即表示64个值需要6个地址线的意思)。BITS_PER_WORD是1算数左移6位,即1 × 2^6 = 64,意为一个“字”(long)包含64个bit。BIT_INDEX_MASK是63,即16进制的0x3f,可理解成低6位全为1。WORD_MASK,全1,用于掩码。

  至于为什么选择long这种数据类型,注释的解析是基于性能的原因,现在64位CPU已经非常普及,可以一次把一个64bit长度的long放进寄存器作计算。

     **
* The internal field corresponding to the serialField "bits".
*/
private long[] words;

  属性words即为实际存储数据的地方.

2.一些公共函数

     /**
* Given a bit index, return word index containing it.
*/
private static int wordIndex(int bitIndex) {
return bitIndex >> ADDRESS_BITS_PER_WORD;
}

  这个静态函数在很多其它函数中会用到,用途是传入一个bit的索引值bitIndex,返回这个bit所在的那个long在long[]中的索引值。就是把bitIndex算数右移6位,也就是bitIndex除以64,因为long长度是64bit。比如第50个bit所对应的long就是50 / 64 = 0,即words中的第0个long。

3.构造函数

     /**
* Creates a new bit set. All bits are initially {@code false}.
*/
public BitSet() {
initWords(BITS_PER_WORD);
sizeIsSticky = false;
} /**
* Creates a bit set whose initial size is large enough to explicitly
* represent bits with indices in the range {@code 0} through
* {@code nbits-1}. All bits are initially {@code false}.
*
* @param nbits the initial size of the bit set
* @throws NegativeArraySizeException if the specified initial size
* is negative
*/
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;
} private void initWords(int nbits) {
words = new long[wordIndex(nbits-1) + 1];
}

  如果用户调用默认构造函数,则会分配一个长度为64bit的BitSet,如果BitSet(int nbits),则会分配一个大于等于nbits并且是64的整数倍的BitSet,比如调用BitSet(100),则会分配长度为128的BitSet(即2个long)。

public static BitSet valueOf(long[] longs)
public static BitSet valueOf(LongBuffer lb)
public static BitSet valueOf(byte[] bytes)
public static BitSet valueOf(ByteBuffer bb)

  BitSet也提供了一些静态函数让用户从一些已有的数据结构中直接构造BitSet。注意上面4个函数都是会把传入参数拷贝一个副本以供BitSet自己使用,所以并不会改变传入参数的数据。

4.动态扩展容量

  上一篇Blog提到过,BitSet能够在一些操作(如Set())的时候,如果传入参数大于BitSet本身已有的长度,则它会自动扩展到所需长度。主要以来下面的函数:

     /**
* Ensures that the BitSet can hold enough words.
* @param wordsRequired the minimum acceptable number of words.
*/
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;
}
}

  这个函数的传入参数wordsRequired表示需要多少个“字”,它会与当前words的长度作比较,如果wordsRequired比较大的话,则会新建一个long[],长度取当前words长度的2倍与wordsRequired中较大的那个值,最后把当前words的内容拷贝到新long[]中,并把这个words指向这个新long[]。这就完成了动态扩容,跟ArrayList的实现方式非常类似,另一方面也看到这份代码不是线程安全的,多线程竞争下必须用户手动同步。

5.flip反转某一位

     /**
* Sets the bit at the specified index to the complement of its
* current value.
*
* @param bitIndex the index of the bit to flip
* @throws IndexOutOfBoundsException if the specified index is negative
* @since 1.4
*/
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();
}

  flip函数提供反转某一个位的功能。做法是先找到bitIndex所在的long,然后把这个long跟(1L << bitIndex)做“异或”操作(XOR)。注意bitIndex是可以大于63的,左移运算符(<<)支持循环移位,即实际左移位数为(bitIndex%64)这么多。假设用户调用flip(66),则代码先找到wordIndex = 1,即words[1]这个long。然后(1L << bitIndex)就是(1L << (66%64))即(1L << 2)= 0b0100,从低位数起第3个位为1,其余都为0。最后把words[1]跟0b0100做XOR,因为布尔运算中一个值与1做XOR的结果就是这个值的反,而与0做异或则不变,所以words[1]的第3位被取反了。

6.clear清除某一个位的值

     /**
* Sets the bit specified by the index to {@code false}.
*
* @param bitIndex the index of the bit to be cleared
* @throws IndexOutOfBoundsException if the specified index is negative
* @since JDK1.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();
}

  其实也就是把某一个位设为0。过程与上面flip类似,但进行的位运算不一样,这里是把(1L << bitIndex)取反再跟words[wordIndex]进行“与”运算(AND)。原理其实很简单,布尔运算中一个值和1做AND运算,则其值不变;而如果和0做AND运算,则结果为0。比如:1100 & ~(0100) 等于 1100 & 1011 = 1000.

  另外BitSet还提供了get, set接口、跟另一个BitSet对象做AND/OR/XOR运算的接口,这些都是用到位运算,比较好理解,不再赘述,请自行参考API.

Java数据结构: java.util.BitSet源码学习的更多相关文章

  1. 【Java】java.util.Objects 源码学习

    2017-02-10 by 安静的下雪天  http://www.cnblogs.com/quiet-snowy-day/p/6387321.html    本篇概要 Objects 与 Object ...

  2. Java数据结构和算法 - TreeMap源码理解红黑树

    前言 本篇将结合JDK1.6的TreeMap源码,来一起探索红-黑树的奥秘.红黑树是解决二叉搜索树的非平衡问题. 当插入(或者删除)一个新节点时,为了使树保持平衡,必须遵循一定的规则,这个规则就是红- ...

  3. Java并发包源码学习系列:阻塞队列BlockingQueue及实现原理分析

    目录 本篇要点 什么是阻塞队列 阻塞队列提供的方法 阻塞队列的七种实现 TransferQueue和BlockingQueue的区别 1.ArrayBlockingQueue 2.LinkedBloc ...

  4. Java集合专题总结(1):HashMap 和 HashTable 源码学习和面试总结

    2017年的秋招彻底结束了,感觉Java上面的最常见的集合相关的问题就是hash--系列和一些常用并发集合和队列,堆等结合算法一起考察,不完全统计,本人经历:先后百度.唯品会.58同城.新浪微博.趣分 ...

  5. Java并发包源码学习之AQS框架(一)概述

    AQS其实就是java.util.concurrent.locks.AbstractQueuedSynchronizer这个类. 阅读Java的并发包源码你会发现这个类是整个java.util.con ...

  6. 【JDK1.8】 Java小白的源码学习系列:HashMap

    目录 Java小白的源码学习系列:HashMap 官方文档解读 基本数据结构 基本源码解读 基本成员变量 构造器 巧妙的tableSizeFor put方法 巧妙的hash方法 JDK1.8的putV ...

  7. Java并发包源码学习系列:ReentrantLock可重入独占锁详解

    目录 基本用法介绍 继承体系 构造方法 state状态表示 获取锁 void lock()方法 NonfairSync FairSync 公平与非公平策略的差异 void lockInterrupti ...

  8. Java并发包源码学习系列:ReentrantReadWriteLock读写锁解析

    目录 ReadWriteLock读写锁概述 读写锁案例 ReentrantReadWriteLock架构总览 Sync重要字段及内部类表示 写锁的获取 void lock() boolean writ ...

  9. Java并发包源码学习系列:详解Condition条件队列、signal和await

    目录 Condition接口 AQS条件变量的支持之ConditionObject内部类 回顾AQS中的Node void await() 添加到条件队列 Node addConditionWaite ...

随机推荐

  1. Bootstrap风格登录界面设计样例

    参考:http://bootsnipp.com/tags/login Register Page   127.8K 187 Modal Login with jQuery Effects   159. ...

  2. 齐B小短裙

    女模周蕊微博引关注 火了齐B小短裙毁了干爹[图]_网易新闻中心 齐B小短裙

  3. Annotation(一)——注解开发介绍

    <p>在编程中,一直强调的一点就是注释的编写,注释的规范等等.尤其是越是核心,程序越复杂,逻辑越多的清空下,注释的编写对我们以后的阅读代码,维护软件起着至关重要的作用.一款软件有着好的注释 ...

  4. Django之验证码 + session 认证

    验证码 + session认证 目录结构 . └── project ├── app01 │   ├── admin.py │   ├── apps.py │   ├── __init__.py │  ...

  5. java基础 二分查找算法

    /*   * 折半查找法:   * 思路:   * 定义三个变量记录查找范围中最大.最小和中间的索引值,每次都是使用中间索引值与要查找的目标进行对比,如果不符合,那么就不停缩小查找范围   * */  ...

  6. PCM文件格式简单介绍

    PCM文件格式简单介绍 PCM文件:模拟音频信号经模数转换(A/D变换)直接形成的二进制序列,该文件没有附加的文件头和文件结束标志.Windows的Convert工具能够把PCM音频格式的文件转换成M ...

  7. c语言10个经典小程序

    [程序1] 题目:有1.2.3.4个数字,能组成多少个互不相同且无重复数字的三位数?都是多少? 1.程序分析:可填在百位.十位.个位的数字都是1.2.3.4.组成所有的排列后再去 掉不满足条件的排列. ...

  8. 浅谈GitLab与Git

    前言:先解释下关于库的认识. Workspace:工作区 Index / Stage:暂存区 Repository:仓库区(或本地仓库) Remote:远程仓库 一.新增项目(远程仓库) 在GitLa ...

  9. 未能加载文件或程序集“System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35”

    最近用vs2012发布程序,然后将更新后的程序文件部署到服务器上,由于服务器上本来有此系统,所以只更新了修改的文件 . 进行系统登录时提示:未能加载文件或程序集“System.Web.Extensio ...

  10. get请求与post请求

    1. get是从服务器上获取数据,post是向服务器传送数据.2. get是把参数数据队列加到提交表单的ACTION属性所指的URL中,值和表单内各个字段一一对应,在URL中可以看到.post是通过H ...