文章目录

  • 引言
  • 一、BitSet是什么?
  • 二、BitSet 常用方法
  • 三、BitSet 源码解析
    • 1、初始化
    • 2、set(int bitIndex) 源码
    • 3、get(int bitIndex) 源码
    • 4、clear(int bitIndex) 源码
    • 5、flip(int bitIndex) 源码
    • 5、set(int fromIndex, int toIndex) 源码
    • 6、and(BitSet set) 源码
    • 7、nextClearBit(int fromIndex)源码
  • 总结

引言

ArrayList 提供了一个方法 removeIf

其源码实现中,巧用 BitSet,惊艳到我了。

于是乎,拜读 BitSet 源码,位运算用的真6,服!!!


一、BitSet是什么?

我们常说的位图,在JAVA 中的实现,是 BitSet

也可以说是一种算法吧,很突出点:省空间

什么意思呢?举个简单的例子吧。

比如说有这么个场景:某基金的交易日记为1,休息日记为0,
那要记录一整年的数据,那就是 365 个数字,由1和0组成。
若数字是 int 类型,那 365 个数字,就是 1460 字节。
如果用 BitSet 来记录,理论上 48 个字节就可以了。

BitSet 使用 long 数组来记录数据,
long 8 个字节, 64 位,每位可对应一天的数据

比如第1天是交易日,在 long 的第 1 位,记录为 1,

第2天是休息日,在 long 的第 2 位,记录为 0,

以此类推,365 天, 6 个 long 就搞定。

BitSet 提供了一系列的方法,

封装位运算,方便使用。

这里写了个小 Demo,

图中 set (2) 就是 long 的 第 2 位 设置为 1

set (7) 就是 long 的 第 7 位 设置为 1,

bitSet.get(2) ,显示为 true该位是 1,就会返回 true
bitSet.get(5) ,显示为 false该位是 0,就会返回 false

先简单介绍下API,有个印象,之后再分析源码。

二、BitSet 常用方法

1、set(int bitIndex)
. 将对应下标位置的值设置为1

前面截图中的例子,说的不严谨,应该说下标位置。

调用 set (2)set (7) 后,对应的 long ,二进制应该是:10000100

如果调用 set (66) 时,那会怎么样呢?

一个 long 是 64 位,不够用了,

那会在 long 数组的第二个元素中进行操作,

也就是第二个 long,下标为 2 的位置,设置为1,

即 第二个 long 的二进制是:100

2、get(int bitIndex)
. 判断对应下标处是否为1,是1 返回 true, 否则返回 false

比如截图中的例子, get(2) 返回 true

调用 set (66) 时,会判断 第二个 long

其下标为2 的位置是否为1。

3、clear(int bitIndex))
. 将对应下标处的值清除

其实从方法的命名,也能猜到。说白了,就是把对应的下标处,设置为 0。

4、flip(int bitIndex))
. 将对应下标处的值反转

某下标处是1,调用该方法后变为0,
同样,本来是0,调用之后就变为1。

5、nextSetBit(int bitIndex))
. 从某下标处开始,第1个值为1的下标是多少

比如说,还是截图中的例子,调用 bitSet.nextSetBit(2),就会返回2
从下标为2开始判断,哪个位置值为1,当然就是 2 了。

调用 bitSet.nextSetBit(4),就会返回7,也很简单,
下标 4、5、6 的值都是 0,首次值为1,是下标7,所以返回 7。

如果不存在值为1的情况怎么办呢?
比如截图中的情况,调用 bitSet.nextSetBit(10)
返回 -1。

6、nextClearBit(int bitIndex))
. 从某下标处开始,第1个值为0的下标是多少

这个与上面的那个类似,不多解释了。

7、previousSetBit(int bitIndex))
8、previousClearBit(int bitIndex))

不多解释
下面几个是求交集、并集、补集,差集的

09、and(BitSet set) ----- 两者交集
10、or(BitSet set) ------- 两者全集
09、xor(BitSet set) ------- 两者全集减去交集,剩下的
10、andNot(BitSet set) ------- 前面的bit,去掉交集剩余的


三、BitSet 源码解析

1、初始化

初始化(不指定大小):BitSet bitSet = new BitSet();
初始化(指定了大小):BitSet bitSet = new BitSet(30);

初始化的相关代码,粘贴出来了


public class BitSet implements Cloneable, java.io.Serializable {private final static int ADDRESS_BITS_PER_WORD = 6;private final static int BITS_PER_WORD = 1 << ADDRESS_BITS_PER_WORD;private long[] words; // long 数组private transient int wordsInUse = 0;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;}
}

不指定大小时,初始化后 words 的长度为1。

指定了大小,初始化后 words 的长度,可以认为是 (n/64)+1

比如初始化时,传入30,可以认为要记录 30 个数据。

一个 long 是 64 位,最大可以记录64个数据。

要记录30 个数据,一个 long 就可以了。

直观的可以看这个图,传入是 n 时,

若 n%64 == 0 , 那需要 long 的个数就是 n/64
若 n%64 != 0 , 那需要 long 的个数就是 (n/64)+1

这两种情况 与 ((n-1)/64)+1 等价。

代码中 bitIndex >> ADDRESS_BITS_PER_WORD 这个就是除以64的意思。

n/64n >> 6 是等价的,

如果不是很清楚,问下度娘吧,要不留言也行。

总结一句:初始化时,确定 long 数组大小。

2、set(int bitIndex) 源码

前面说过,这个方法是,将某下标处的值,设置为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 invariantscheckInvariants();
}

int wordIndex = wordIndex(bitIndex); 这个是算出,该坐标,是第几个 long.

这个方法,上面画图说过了,大概就是 除以64 的意思。

expandTo(wordIndex); 这个方法是自动扩容的,本篇不细说了。

比如说 set(int bitIndex) ,传入的是 3。

按位或操作的性质:

下标为3的那个位置,计算出的结果一定是 1,
其下标位置的值,一定不变。

再比如说 set(int bitIndex) ,传入的是 67

int wordIndex = wordIndex(bitIndex); 这里 wordIndex 就是 1

1L << 671L << 3相等 的,其它的不用多解释了。

3、get(int bitIndex) 源码

前面说过,这个方法是,判断对应下标处,是不是1,
是1 返回 true, 否则返回 false

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

wordIndex 是根据入参,算出该坐标,是第几个 long.

wordsInUse 这个前面没说,它表示 words 的实际长度,即总共有几个 long

wordIndex < wordsInUse 这个是判断是否下标越界,

如果越界直接返回 false

这个不难理解,自己琢磨下,实在不懂留言里问吧!

(words[wordIndex] & (1L << bitIndex)) != 0,这句画图解释下

假如 箭头所指的位置是 bitIndex, 按位与操作的性质,
其它下标处,结果一定为0,

bitIndex 下标处的值,问号是1,结果就是1,问号是0,结果就是0。

通过巧妙的位运算,就判断出某下标处,是否为1。

4、clear(int bitIndex) 源码

前面说过,这个方法就是,将对应下标处的值清除

所谓清除,就是设置为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();}private void recalculateWordsInUse() {int i;for (i = wordsInUse-1; i >= 0; i--)if (words[i] != 0)break;wordsInUse = i+1; // The new logical size}

int wordIndex = wordIndex(bitIndex); 这个是算出,该坐标,是第几个 long

words[wordIndex] &= ~(1L << bitIndex); 这个也画个图解释。

假如 箭头所指的位置是 bitIndex, 按位与操作的性质

index 下标处的值,一定为0,其它位的值一定不变

recalculateWordsInUse 这个方法简单说下,

当把某坐标处的值,设置为0后,有可能整个long 的值变为0,

这时要重新计算 wordsInUse

5、flip(int bitIndex) 源码

将对应下标处的值反转

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

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


按位异或操作,相同为0,不同为1。

结合图来看, 箭头处,值反转,其它下标处的值,保持原样。

至此为止,对某一下标处的操作,几个方法都讲完了,

这位运算封装的很好,你可以直接调用就好了。

下面看下范围操作!也很好玩儿。

5、set(int fromIndex, int toIndex) 源码

这个方法是将,某一范围的值,都设置为1(包头不包尾)

public void set(int fromIndex, int toIndex) {checkRange(fromIndex, toIndex);if (fromIndex == toIndex)return;int startWordIndex = wordIndex(fromIndex);int endWordIndex   = wordIndex(toIndex - 1);expandTo(endWordIndex); // 必要情况下,扩容long firstWordMask = WORD_MASK << fromIndex;long lastWordMask  = WORD_MASK >>> -toIndex;if (startWordIndex == endWordIndex) {words[startWordIndex] |= (firstWordMask & lastWordMask);} else {words[startWordIndex] |= firstWordMask;for (int i = startWordIndex+1; i < endWordIndex; i++)words[i] = WORD_MASK;words[endWordIndex] |= lastWordMask;}checkInvariants();
}

这分为两种情况,

  • bitSet.set(5, 8) , 范围落在同一个 long 上,
  • bitSet.set(60, 80) , 范围跨越不同的 long

先说第一种情况哈

firstWordMask & lastWordMask 运算的结果,就是下标为 5,6,7 为1,其它都为0,

最后, 按位或操作,使 words[0] 的 5,6,7 位都设置为1,其它都不变。

别问作者是怎么写出来的,反正我写不出来,

我相信,我不孤独,绝大多数的人,都写不出来!

另外一种情况是跨越不同的 long

处理首尾两个 long, 方法类似,不再画图了。

之间的 long,设置为 -1,即 64 位都是1,不需要位运算了。

6、and(BitSet set) 源码

这个方法前面说过,是求交集。

 public void and(BitSet set) {if (this == set)return;while (wordsInUse > set.wordsInUse)words[--wordsInUse] = 0;// Perform logical AND on words in commonfor (int i = 0; i < wordsInUse; i++)words[i] &= set.words[i];recalculateWordsInUse();checkInvariants();}

这个比较好理解,

while 循环,是将多出来的 long 都设置为0。
多出来的,肯定不是交集。

对应下标的 long 取交集,即 按位与操作

最后重新计算 wordsInUse

其它 几个集合运算的方法,套路都差不多,略。

7、nextClearBit(int fromIndex)源码

这个方法前面说过,是从某下标处开始,第1个值为0的下标是多少


public int nextClearBit(int fromIndex) {if (fromIndex < 0)throw new IndexOutOfBoundsException("fromIndex < 0: " + fromIndex);checkInvariants();int u = wordIndex(fromIndex); // 算出是第几个 long if (u >= wordsInUse)return fromIndex;  // 越界说明该下标处是0,直接返回long word = ~words[u] & (WORD_MASK << fromIndex);while (true) {if (word != 0) // 目标下标,就在当前 long 中return (u * BITS_PER_WORD) + Long.numberOfTrailingZeros(word);if (++u == wordsInUse)return wordsInUse * BITS_PER_WORD;word = ~words[u];}
}

这个思路很巧妙,画图更容易懂,假设 fromIndex 是5

假设 words[0] 是上面这个样子,箭头处是下标5的位置,

那肉眼可见,从 5 开始,第一个0的位置,下标是8。

通过位运算,把下标 0~5 设置为0,其余的 0 和 1 翻转,

下标8的位置是1,之前的全部是 0 。

Long.numberOfTrailingZeros(word)

这个方法,就是返回低位有几个连续的0。
比如 二进制 11100,会返回 2,
比如 二进制 10101000,会返回 3,

u * BITS_PER_WORD 就是 u * 64 ,这个不多解释。

if (++u == wordsInUse) 这个意思是,最后一个 long,所有位上都是1。

这样的位运算,我实在是想不到,服气!

previousSetBit(int bitIndex)) previousClearBit(int bitIndex)) 套路差不多,略!


总结

  1. BitSet 简单介绍,它是一种算法吧,用 位 来记录数据,省空间。封装 位运算。
  2. BitSet 常用的API,差不多是增删除改查,还支持范围操作。
  3. BitSet 的源码解析,主要分析了位运算的效果。

至于 BitSet 的应用,单独写了一篇《BitSet》。OVER!!!

BitSet源码解析,位运算玩的真六相关推荐

  1. JavaScript数字运算必备库——big.js源码解析

    概述 在我们常见的JavaScript数字运算中,小数和大数都是会让我们比较头疼的两个数据类型. 在大数运算中,由于number类型的数字长度限制,我们经常会遇到超出范围的情况.比如在我们传递Long ...

  2. Robot Arm 机械臂源码解析

    Robot Arm 机械臂源码解析 说明: ​ Robot Arm是我复刻,也是玩的第一款机械臂.用的是三自由度的结构,你可以理解为了三个电机,三轴有自己的一些缺陷.相比于六轴机械臂而言因为结构的缺陷 ...

  3. 死磕Java集合之BitSet源码分析(JDK18)

    死磕Java集合之BitSet源码分析(JDK18) 文章目录 死磕Java集合之BitSet源码分析(JDK18) 简介 继承体系 存储结构 源码解析 属性 构造方法 set(int bitInde ...

  4. Java集合---LinkedList源码解析

    一.源码解析 1. LinkedList类定义 2.LinkedList数据结构原理 3.私有属性 4.构造方法 5.元素添加add()及原理 6.删除数据remove() 7.数据获取get() 8 ...

  5. Java HashSet源码解析

    本解析源码来自JDK1.7,HashSet是基于HashMap实现的,方法实现大都直接调用HashMap的方法 另一篇HashMap的源码解析文章 概要 实现了Set接口,实际是靠HashMap实现的 ...

  6. HashMap源码解析(JDK1.8)

    HashMap源码解析(JDK1.8) 目录 定义 构造函数 数据结构 存储实现源码分析 删除操作源码分析 hashMap遍历和异常解析 1. 定义 HashMap实现了Map接口,继承Abstrac ...

  7. 增加数组下标_数组以及ArrayList源码解析

    点击上方"码之初"关注,···选择"设为星标" 与精品技术文章不期而遇 前言 前一篇我们对数据结构有了个整体的概念上的了解,没看过的小伙伴们可以看我的上篇文章: ...

  8. js怎么调用wasm_Long.js源码解析

    基于现在市面上到处都是 Vue/React 之类的源码分析文章实在是太多了.(虽然我也写过 Vite的源码解析 所以这次来写点不一样的.由于微信这边用的是 protobuf 来进行 rpc 调用.所以 ...

  9. dubbo源码解析(十)远程通信——Exchange层

    远程通讯--Exchange层 目标:介绍Exchange层的相关设计和逻辑.介绍dubbo-remoting-api中的exchange包内的源码解析. 前言 上一篇文章我讲的是dubbo框架设计中 ...

  10. java容器三:HashMap源码解析

    前言:Map接口 map是一个存储键值对的集合,实现了Map接口的主要类有以下几种 TreeMap:用红黑树实现 HashMap:数组和链表实现 HashTable:与HashMap类似,但是线程安全 ...

最新文章

  1. Thinkpad R400 a16驱动安装笔记
  2. oracle 高水位线回收,回收高水位线
  3. voting设计模式
  4. python获取当前路径的方法_Python获取脚本所在目录的正确方法【转】
  5. Java中的标识符及其命名规则
  6. 尽说大实话!周鸿祎:有的软件会偷偷打开你的摄像头或麦克风
  7. tf.cast()的用法(转)
  8. 微信小程序源码免费下载
  9. sd卡卡槽_还在傻傻分不清楚SD卡、Micro SD、TF卡?卡槽马上都要取消了
  10. 助力运动:实时乒乓球视频分析
  11. Ubuntu搭建BT服务器FTP服务器发布种子
  12. IE8 证书错误,导航已阻止,解决方法(selenium)
  13. 面试经验|华为二面分享 真难ε=(´ο`*)))唉
  14. 时统ptp_IEEE1588 PTP对时系统原理及特点
  15. openssl下载与安装
  16. python 保存数据为excel
  17. 【源码】loess method扩展算法仿真
  18. 一站式、整套智能家居解决方案——HomeKit?绿米?华为还是智汀?
  19. 小白入门篇:量化大神Eric跟你聊量化交易
  20. 计算机技能大赛简讯内,科技节现场类比赛简讯

热门文章

  1. npm install 设置缓存
  2. 如何通过波形解析can总线数据
  3. Python datetime日期相减
  4. Linux关闭桌面进程,Centos进入桌面和退出桌面的方法
  5. 百词斩不复习_也说说百词斩的缺点
  6. php面向对象编程孙卫琴,什么是面向对象编程(OOP)?,面向对象编程孙卫琴
  7. 基于文本语义的智能问答机器人——工业应用
  8. matlab中停止调试快捷键,matlab停止运行快捷键
  9. 微信小程序入门12-微信小程序开发设置中服务器域名和业务域名
  10. ipv4.method