目录
[2.1 Java 中的位运算符](#2.1 Java 中的位运算符)
[2.2 四个核心位操作的原理](#2.2 四个核心位操作的原理)
[三、工具类设计:32 位 int 版本](#三、工具类设计:32 位 int 版本)
[3.1 完整代码](#3.1 完整代码)
[3.2 设计要点解析](#3.2 设计要点解析)
[四、工具类设计:64 位 long 版本](#四、工具类设计:64 位 long 版本)
[4.1 完整代码](#4.1 完整代码)
[4.2 关键细节:1L << pos 而非 1 << pos](#4.2 关键细节:1L << pos 而非 1 << pos)
[4.3 为什么上限是 62 而不是 63?](#4.3 为什么上限是 62 而不是 63?)
[4.4 >> 与 >>> 的选择](#4.4 >> 与 >>> 的选择)
[5.1 枚举定义](#5.1 枚举定义)
[5.2 业务代码中的调用方式](#5.2 业务代码中的调用方式)
[5.3 这种设计的优势](#5.3 这种设计的优势)
[6.1 抽象处理器设计](#6.1 抽象处理器设计)
[6.2 具体实现示例](#6.2 具体实现示例)
[6.3 模板方法的价值](#6.3 模板方法的价值)
[7.1 场景一:商品属性标记(int 版本)](#7.1 场景一:商品属性标记(int 版本))
[7.2 场景二:运营活动奖励领取记录(long 版本)](#7.2 场景二:运营活动奖励领取记录(long 版本))
[7.3 场景三:签到打卡记录(long 版本)](#7.3 场景三:签到打卡记录(long 版本))
[7.4 场景对比总结](#7.4 场景对比总结)
[8.1 int 还是 long?如何选择](#8.1 int 还是 long?如何选择)
[8.2 参数校验不可省略](#8.2 参数校验不可省略)
[8.3 并发安全问题](#8.3 并发安全问题)
[8.4 数据库查询的局限性](#8.4 数据库查询的局限性)
[8.5 统一工具类命名](#8.5 统一工具类命名)
一、前言:为什么要关注位运算?

在日常 Java 后端开发中,我们经常遇到这样的场景:一个实体需要记录大量布尔状态。比如用户是否完成了某个任务、是否领取了某个奖励、是否触发了某个特殊条件......
最直觉的做法是为每个状态单独建一个数据库字段:
is_task1_done TINYINT(1)
is_task2_done TINYINT(1)
is_task3_done TINYINT(1)
...
is_task30_done TINYINT(1)
这种方式在状态数量少的时候没有问题,但一旦状态数量膨胀到 十几个甚至几十个,问题就来了:
- 表结构臃肿 :每新增一个状态就要
ALTER TABLE加字段,线上大表加列的代价非常高 - 存储浪费 :MySQL 中即使是
TINYINT(1),每行也要占用 1 个字节,30 个布尔状态就是 30 字节 - 维护困难 :字段命名容易混乱,代码中到处是
getIsXxx()/setIsXxx(),可读性差
而位运算提供了一种极其优雅的替代方案 :用一个 int(32 位)或 long(64 位)字段,就能存储 32 个甚至 63 个布尔状态 。每个状态只占 1 个 bit,一个 bigint 字段仅 8 字节就能承载 63 个标记位,相比传统方案节省了数倍的存储空间。
更重要的是,位运算是 CPU 原生支持的操作,执行效率极高,不需要任何额外的计算开销。
本文将从零开始,带你设计并封装一套通用的位运算工具类,配合枚举管理位语义,让你的代码既高效又优雅。
二、位运算基础回顾
在深入工具类设计之前,我们先快速回顾一下 Java 中位运算的核心操作。如果你对这些已经很熟悉,可以直接跳到第三节。
2.1 Java 中的位运算符
|-------|-------|----------------|----------------------------|
| 运算符 | 名称 | 说明 | 示例 |
| & | 按位与 | 两位都为 1 时结果为 1 | 0b1010 & 0b1100 = 0b1000 |
| | | 按位或 | 任一位为 1 时结果为 1 | 0b1010 | 0b1100 = 0b1110 |
| ^ | 按位异或 | 两位不同时结果为 1 | 0b1010 ^ 0b1100 = 0b0110 |
| ~ | 按位取反 | 0 变 1,1 变 0 | ~0b1010 = 0b...0101 |
| << | 左移 | 各位左移指定位数,低位补 0 | 1 << 3 = 0b1000 = 8 |
| >> | 有符号右移 | 各位右移,高位补符号位 | -8 >> 2 = -2 |
| >>> | 无符号右移 | 各位右移,高位补 0 | -1 >>> 28 = 15 |
2.2 四个核心位操作的原理
理解了运算符之后,我们来看位运算工具类最核心的四个操作:
① 置 1(Set Bit)------ 将指定位设为 1
原理:num | (1 << pos)
1 << pos 生成一个只有第 pos 位为 1 的掩码,然后与原数做或运算 。由于 任何数 | 1 = 1,所以目标位一定会变成 1,而其他位保持不变(因为 任何数 | 0 = 任何数)。
举例:将数字 5(二进制 0101)的第 1 位置 1:
0101 (5)
| 0010 (1 << 1)
------
0111 (7)
② 置 0(Clear Bit)------ 将指定位清零
原理:num & ~(1 << pos)
先通过 ~(1 << pos) 生成一个只有第 pos 位为 0、其余全为 1 的掩码,再与原数做与运算 。由于 任何数 & 0 = 0,目标位被清零;而 任何数 & 1 = 任何数,其他位保持不变。
举例:将数字 7(二进制 0111)的第 1 位清零:
0111 (7)
& 1101 (~(1 << 1))
------
0101 (5)
③ 检查(Check Bit)------ 判断指定位是否为 1
原理:(num & (1 << pos)) != 0
用掩码提取目标位,如果结果不为 0,说明该位是 1。这里有一个细节值得注意:我们判断的是 != 0 而不是 == 1,因为 num & (1 << pos) 的结果可能是 2、4、8 等任何 2 的幂次,而不一定是 1。
④ 取反(Toggle Bit)------ 翻转指定位
原理:num ^ (1 << pos)
利用异或的特性:1 ^ 1 = 0,0 ^ 1 = 1。所以目标位会被翻转,而其他位与 0 异或保持不变。
三、工具类设计:32 位 int 版本
掌握了原理之后,我们来封装第一个工具类,处理 32 位 int类型的位运算场景。
3.1 完整代码
java
/**
* 位运算工具类 - int 版本(32位)
* 操作范围:0 <= pos <= 31
*/
public final class BitUtil {
private BitUtil() {
// 工具类禁止实例化
}
/**
* 将指定位置 1
*
* @param num 原始值
* @param pos 位位置(0~31)
* @return 置 1 后的值
*/
public static int setBit(int num, int pos) {
return num | (1 << pos);
}
/**
* 将指定位清零
*
* @param num 原始值
* @param pos 位位置(0~31)
* @return 清零后的值
*/
public static int clearBit(int num, int pos) {
return num & ~(1 << pos);
}
/**
* 检查指定位是否为 1
*
* @param num 原始值
* @param pos 位位置(0~31)
* @return true 表示该位为 1
*/
public static boolean checkBit(int num, int pos) {
return (num & (1 << pos)) != 0;
}
/**
* 翻转指定位(0→1,1→0)
*
* @param num 原始值
* @param pos 位位置(0~31)
* @return 翻转后的值
*/
public static int toggleBit(int num, int pos) {
return num ^ (1 << pos);
}
/**
* 将指定位设为指定值
*
* @param num 原始值
* @param pos 位位置(0~31)
* @param value 目标值(0 或 1)
* @return 更新后的值
*/
public static int updateBit(int num, int pos, int value) {
// 先清零目标位,再按 value 设值
return (num & ~(1 << pos)) | (value << pos);
}
}
3.2 设计要点解析
为什么用 final类 + 私有构造器?
这是 Java 工具类的标准写法。final 防止被继承,私有构造器防止被实例化。所有方法都是 static 的,通过类名直接调用,这与 java.util.Collections、java.util.Arrays 等 JDK 工具类的设计理念一致。
updateBit方法的巧妙之处
updateBit 是 setBit 和 clearBit 的统一封装。它的实现分两步:
num & ~(1 << pos):先将目标位清零,无论它原来是 0 还是 1| (value << pos):再将value(0 或 1)移到目标位,通过或运算写入
这种「先清后写」的模式非常经典,在硬件编程和嵌入式开发中也广泛使用。
操作范围为什么是 0~31?
Java 的 int 类型是 32 位有符号整数,第 0 位到第 30 位可以安全使用,第 31 位是符号位。虽然技术上可以操作第 31 位,但在大多数业务场景中,我们使用 0~30 位就足够了(31 个标记位)。如果你确实需要使用第 31 位,请确保你的业务逻辑能正确处理负数。
四、工具类设计:64 位 long 版本
当 32 个标记位不够用时,我们需要升级到 64 位 long类型。这里有一个非常关键的细节需要特别注意。
4.1 完整代码
java
/**
* 位运算工具类 - long 版本(64位)
* 操作范围:0 <= pos <= 62
*/
public final class BitUtils {
private BitUtils() {
// 工具类禁止实例化
}
/**
* 获取指定位的值
*
* @param source 原始值
* @param pos 位位置(0~62)
* @return 0 或 1
*/
public static byte getBitValue(long source, int pos) {
return (byte) ((source >> pos) & 1);
}
/**
* 将指定位设为指定值
*
* @param source 原始值
* @param pos 位位置(0~62)
* @param value 目标值(0 或 1)
* @return 更新后的值
*/
public static long setBitValue(long source, int pos, byte value) {
long mask = 1L << pos;
if (value == 1) {
source |= mask;
} else {
source &= ~mask;
}
return source;
}
/**
* 翻转指定位
*
* @param source 原始值
* @param pos 位位置(0~62)
* @return 翻转后的值
*/
public static long reverseBitValue(long source, int pos) {
long mask = 1L << pos;
return source ^ mask;
}
/**
* 检查指定位是否为 1
*
* @param source 原始值
* @param pos 位位置(0~62)
* @return true 表示该位为 1
*/
public static boolean checkBitValue(long source, int pos) {
return ((source >>> pos) & 1) == 1;
}
}
4.2 关键细节:1L << pos 而非 1 << pos
这是 long 版本中最容易踩的坑,也是面试中经常考察的知识点。
在 Java 中,字面量 1 默认是 int 类型。如果你写 1 << 35,Java 会先对移位量取模 32(因为 int 是 32 位),实际执行的是 1 << 3 = 8,而不是你期望的 2^35。
java
// 错误!1 是 int 类型,左移超过 31 位会被截断
long mask = 1 << 35; // 实际等于 1 << 3 = 8
// 正确!1L 是 long 类型,可以安全左移到 62 位
long mask = 1L << 35; // 等于 34359738368
这就是为什么 long 版本的工具类中,所有掩码生成都必须使用 1L << pos。这一个字母 L的差异,可能导致线上数据错乱的严重 Bug。
4.3 为什么上限是 62 而不是 63?
Java 的 long 是 64 位有符号整数,第 63 位(从 0 开始计数)是符号位 。操作符号位会导致数值变为负数,在数据库存储和业务逻辑中都可能引发意想不到的问题。因此,我们将安全操作范围限定在 0~62,共 63 个标记位。
对于绝大多数业务场景来说,63 个标记位已经绰绰有余。如果你真的需要更多,可以考虑使用多个 long 字段或者 BitSet。
4.4 >> 与 >>> 的选择
在 checkBitValue 方法中,我们使用了无符号右移 >>> 而非有符号右移 >>。这是因为:
>>会在高位补符号位:如果source是负数,右移后高位补 1,可能影响判断结果>>>无论正负,高位一律补 0,确保右移后只保留目标位的值
虽然在 & 1 的配合下,两者在大多数情况下结果相同,但使用 >>> 是更严谨的做法,能避免极端情况下的潜在问题。
五、枚举管理位语义------告别魔法数字
工具类解决了「怎么操作」的问题,但还有一个同样重要的问题:每个 bit 位代表什么含义?
如果在代码中直接写 BitUtils.checkBitValue(records, 3),这个 3 是什么意思?一周后你自己可能都忘了。这就是典型的**魔法数字(Magic Number)**问题。
解决方案是使用枚举 + pos 字段的设计模式:
5.1 枚举定义
java
/**
* 用户账户状态枚举
* 每个枚举项映射一个 bit 位,用于标记用户账户的各种状态
*/
public enum UserAccountFlagEnum {
/** 第 0 位:是否已完成实名认证 */
REAL_NAME_VERIFIED(0),
/** 第 1 位:是否已绑定手机号 */
PHONE_BINDIED(1),
/** 第 2 位:是否已完成新手引导 */
NEWBIE_GUIDE_COMPLETED(2),
/** 第 3 位:是否已领取注册礼包 */
REGISTER_GIFT_CLAIMED(3),
/** 第 4 位:是否开启了两步验证 */
TWO_FACTOR_ENABLED(4),
;
private final int pos;
UserAccountFlagEnum(int pos) {
this.pos = pos;
}
public int getPos() {
return pos;
}
}
5.2 业务代码中的调用方式
java
// 检查用户是否已完成实名认证
boolean verified = BitUtils.checkBitValue(
user.getAccountFlags(),
UserAccountFlagEnum.REAL_NAME_VERIFIED.getPos()
);
// 标记用户已领取注册礼包
long newFlags = BitUtils.setBitValue(
user.getAccountFlags(),
UserAccountFlagEnum.REGISTER_GIFT_CLAIMED.getPos(),
(byte) 1
);
user.setAccountFlags(newFlags);
5.3 这种设计的优势
① 语义清晰,自文档化
UserAccountFlagEnum.REAL_NAME_VERIFIED.getPos() 比 0 的可读性强了不止一个量级。任何开发者看到这行代码,都能立即理解它的业务含义。
② 编译期安全
枚举是类型安全的。如果你拼错了枚举名,编译器会直接报错;而如果你写错了数字 3,编译器不会有任何提示。
③ 集中管理,防止冲突
所有 bit 位的分配都集中在一个枚举类中,一目了然。新增状态时,只需要在枚举中追加一项,不会出现两个开发者不小心使用了同一个 pos 的情况。
④ 便于扩展
枚举中还可以添加更多属性,比如中文描述、是否可逆等:
java
public enum UserAccountFlagEnum {
REAL_NAME_VERIFIED(0, "实名认证", false),
PHONE_BINDIED(1, "绑定手机", true),
;
private final int pos;
private final String desc;
private final boolean reversible;
// 构造器和 getter 省略
}
六、进阶封装:模板方法模式
在实际项目中,位运算的使用往往遵循一个固定的流程:检查 → 翻转 → 持久化。比如:
- 检查用户是否已经领取过某个奖励(checkBit)
- 如果没有领取,将对应位置 1(setBit)
- 将更新后的值写回数据库(update)
这个流程在不同的业务场景中反复出现,只是「检查哪个位」和「触发条件」不同。这正是模板方法模式的经典应用场景。
6.1 抽象处理器设计
java
/**
* 位运算记录处理器 - 抽象模板
* 封装"检查→翻转→持久化"的通用流程
* 注意:此模板适用于"只增不改"的场景,即位一旦置 1 就不会再清零
*/
public abstract class AbstractBitRecordHandler<T> {
/**
* 模板方法:处理位记录
*
* @param entity 业务实体
* @return true 表示本次处理触发了状态变更
*/
public final boolean handle(T entity) {
long currentRecords = getRecords(entity);
int pos = bitFlagEnum().getPos();
// 1. 检查:如果已经标记过,直接返回
if (BitUtils.checkBitValue(currentRecords, pos)) {
return false;
}
// 2. 校验:检查业务条件是否满足
if (!checkCondition(entity)) {
return false;
}
// 3. 翻转:将目标位置 1
long newRecords = BitUtils.setBitValue(currentRecords, pos, (byte) 1);
// 4. 持久化:写回数据库
saveRecords(entity, newRecords);
// 5. 后置处理:执行额外的业务逻辑(如发奖励、推送通知)
afterHandle(entity);
return true;
}
/** 获取当前记录值 */
protected abstract long getRecords(T entity);
/** 保存更新后的记录值 */
protected abstract void saveRecords(T entity, long newRecords);
/** 返回对应的枚举项(决定操作哪个位) */
protected abstract UserAccountFlagEnum bitFlagEnum();
/** 检查业务条件是否满足 */
protected abstract boolean checkCondition(T entity);
/** 后置处理钩子,默认空实现 */
protected void afterHandle(T entity) {
// 子类可选覆盖
}
}
6.2 具体实现示例
java
/**
* 注册礼包领取处理器
*/
public class RegisterGiftHandler extends AbstractBitRecordHandler<UserAccount> {
@Resource
private UserAccountMapper userAccountMapper;
@Resource
private GiftService giftService;
@Override
protected long getRecords(UserAccount entity) {
return entity.getAccountFlags();
}
@Override
protected void saveRecords(UserAccount entity, long newRecords) {
entity.setAccountFlags(newRecords);
userAccountMapper.updateAccountFlags(entity);
}
@Override
protected UserAccountFlagEnum bitFlagEnum() {
return UserAccountFlagEnum.REGISTER_GIFT_CLAIMED;
}
@Override
protected boolean checkCondition(UserAccount entity) {
// 检查账号注册是否超过 7 天(7 天内可领取)
return Duration.between(entity.getCreateTime(),
LocalDateTime.now()).toDays() <= 7;
}
@Override
protected void afterHandle(UserAccount entity) {
// 发放注册礼包
giftService.grantRegisterGift(entity.getUserId());
}
}
6.3 模板方法的价值
这种设计的核心价值在于:
- 消除重复代码:「检查→翻转→持久化」的流程只写一次,所有子类复用
- 强制规范 :
handle方法用final修饰,子类无法改变处理流程,确保所有记录处理都遵循统一的模式 - 扩展方便:新增一种记录处理,只需创建一个新的子类,实现几个抽象方法即可
- 适合「只增不改」场景:很多业务标记一旦触发就不会撤销(如「是否领取过」),这种模板天然适配
七、实际应用场景分析
让我们来看看位运算工具类在实际项目中的典型应用场景,帮助大家理解什么时候该用、怎么用。
7.1 场景一:商品属性标记(int 版本)
适用情况 :状态数量较少(≤31 个),使用 int 类型存储。
java
-- 数据库表设计
ALTER TABLE product ADD COLUMN attributes INT DEFAULT 0 COMMENT '商品属性位图';
java
// 枚举定义
public enum ProductAttributeEnum {
IS_NEW_ARRIVAL(0), // 新品
IS_HOT_SELLING(1), // 热销
IS_RECOMMENDED(2), // 推荐
SUPPORTS_RETURN(3), // 支持退货
SUPPORTS_INSTALLMENT(4), // 支持分期
IS_PRE_SALE(5), // 预售商品
IS_CROSS_BORDER(6), // 跨境商品
;
private final int pos;
// 构造器和 getter 省略
}
// 业务使用
boolean canReturn = BitUtil.checkBit(
product.getAttributes(),
ProductAttributeEnum.SUPPORTS_RETURN.getPos()
);
一个 int 字段就替代了 7 个布尔列,如果后续新增属性,无需修改表结构,只需在枚举中追加即可。
7.2 场景二:运营活动奖励领取记录(long 版本)
适用情况 :标记数量较多或动态增长,使用 long 类型存储。
sql
-- 数据库表设计
ALTER TABLE user_activity ADD COLUMN reward_flags BIGINT DEFAULT 0 COMMENT '奖励领取位图';
java
// 使用 rewardStageId 作为 pos(动态位置)
public void claimReward(UserActivity record, ActivityRewardStage stage) {
int pos = stage.getStageId(); // 每个奖励阶段有唯一的 stageId(0~62)
// 检查是否已领取
if (BitUtils.checkBitValue(record.getRewardFlags(), pos)) {
throw new BusinessException("奖励已领取,请勿重复操作");
}
// 标记为已领取
long newFlags = BitUtils.setBitValue(record.getRewardFlags(), pos, (byte) 1);
record.setRewardFlags(newFlags);
// 发放奖励并持久化
rewardService.grant(record.getUserId(), stage);
userActivityMapper.updateRewardFlags(record);
}
这里有一个巧妙的设计:用 stageId直接作为 bit 位的 pos。这样每个奖励阶段天然对应一个唯一的位,不需要额外的映射关系。只要确保 stageId 在 0~62 范围内即可。
7.3 场景三:签到打卡记录(long 版本)
java
// 用 bit 位标记月度签到情况
// 第 0~30 位分别代表每月第 1~31 天
public void signIn(SignInRecord record, int dayOfMonth) {
int pos = dayOfMonth - 1; // 第 1 天对应第 0 位
long newSignFlags = BitUtils.setBitValue(record.getMonthlySign(), pos, (byte) 1);
record.setMonthlySign(newSignFlags);
}
// 统计本月已签到天数
public int countSignDays(SignInRecord record) {
return Long.bitCount(record.getMonthlySign());
}
// 检查是否连续签到 7 天(第 N 天往前推 7 天)
public boolean isConsecutive7Days(SignInRecord record, int currentDay) {
for (int i = currentDay - 7; i < currentDay; i++) {
if (i < 0) continue;
if (!BitUtils.checkBitValue(record.getMonthlySign(), i)) {
return false;
}
}
return true;
}
这个场景特别有意思:一个 long 字段就能存储一整个月的签到记录,而且利用 Long.bitCount() 可以一行代码统计签到天数,这是 JDK 内置的位计数方法,底层使用了高效的 Hamming Weight 算法。
7.4 场景对比总结
|-------|----------------------|-----------------|
| 维度 | 传统多列方案 | 位运算方案 |
| 存储空间 | N 个状态 = N 个字段(N 字节起) | 1 个字段(4 或 8 字节) |
| 新增状态 | 需要 ALTER TABLE | 只需新增枚举项 |
| 可读性 | 字段名直观 | 需要枚举辅助理解 |
| 查询便利性 | 可直接 WHERE 条件 | 需要位运算函数 |
| 适用场景 | 状态少、需频繁按状态查询 | 状态多、主要在应用层判断 |
八、注意事项与最佳实践
8.1 int 还是 long?如何选择
|-------|-------------|--------------|
| 选择依据 | int(32位) | long(64位) |
| 可用标记位 | 最多 31 个 | 最多 63 个 |
| 数据库字段 | INT(4 字节) | BIGINT(8 字节) |
| 适用场景 | 状态数量确定且 ≤31 | 状态可能增长或 >31 |
建议 :如果当前状态数量不多但未来可能增长,优先选择 long 版本 。从 int 迁移到 long 需要修改数据库字段类型和所有相关代码,成本不低。而 long 的额外 4 字节存储开销几乎可以忽略不计。
8.2 参数校验不可省略
生产环境中,建议在工具类中加入参数校验:
java
public static long setBitValue(long source, int pos, byte value) {
if (pos < 0 || pos > 62) {
throw new IllegalArgumentException("pos must be between 0 and 62, got: " + pos);
}
if (value != 0 && value != 1) {
throw new IllegalArgumentException("value must be 0 or 1, got: " + value);
}
long mask = 1L << pos;
if (value == 1) {
source |= mask;
} else {
source &= ~mask;
}
return source;
}
虽然参数校验会带来微小的性能开销,但它能在开发阶段快速暴露问题 ,避免数据错乱。在性能极度敏感的场景中,可以通过 assert 替代 if-throw,在生产环境关闭断言。
8.3 并发安全问题
位运算本身是无状态的纯函数,工具类的方法是线程安全的。但在实际使用中,「读取→修改→写回」这个完整流程不是原子的。
java
// 线程不安全的写法!
long flags = user.getAccountFlags(); // 读取
flags = BitUtils.setBitValue(flags, pos, (byte) 1); // 修改
user.setAccountFlags(flags); // 写回
userMapper.update(user); // 持久化
如果两个线程同时执行上述代码,可能出现写覆盖问题。解决方案:
- 数据库层面 :使用
UPDATE ... SET flags = flags | (1 << pos)的原子更新语句 - 应用层面:使用分布式锁(如 Redis 锁)保证同一实体的操作串行化
- 乐观锁:通过版本号机制检测并发冲突
8.4 数据库查询的局限性
位运算方案的一个明显劣势是数据库查询不够直观。如果你需要查询「所有支持退货的商品」,SQL 需要这样写:
sql
-- MySQL 支持位运算
SELECT * FROM product WHERE attributes & (1 << 3) != 0;
虽然功能上没有问题,但这种查询无法利用普通索引 。如果某个状态需要频繁作为查询条件,建议还是单独建列并加索引,位运算方案更适合在应用层进行状态判断的场景。
8.5 统一工具类命名
如果项目中同时存在 BitUtil(int 版)和 BitUtils(long 版),命名上容易混淆。建议统一为一个工具类,通过方法重载区分:
java
public final class BitOperator {
// int 版本
public static int setBit(int num, int pos) { ... }
public static boolean checkBit(int num, int pos) { ... }
// long 版本
public static long setBit(long num, int pos) { ... }
public static boolean checkBit(long num, int pos) { ... }
}
这样调用方只需要记住一个类名,Java 编译器会根据参数类型自动选择正确的重载方法。
九、总结
位运算工具类是一个小而美的技术方案,它的核心思想可以用一句话概括:用 bit 位的 0/1 来表示布尔状态,用一个整数字段承载多个标记。
回顾本文的设计要点:
- 两套工具类 :
int版本(31 个标记位)和long版本(63 个标记位),根据业务需求选择 - 枚举管理语义 :通过
枚举 + pos 字段消除魔法数字,提升代码可读性和可维护性 - 模板方法封装:将「检查→翻转→持久化」的通用流程抽象为模板,子类只需关注业务逻辑
- 关键细节 :
1L << pos的必要性、符号位的规避、>>>的使用、并发安全的考量
位运算不是银弹,它最适合的场景是:状态数量多、主要在应用层判断、以「只增不改」为主的标记类需求。在这类场景中,它能显著减少数据库列数、避免频繁的表结构变更,是一种值得掌握的实用技巧。
📌 如果这篇文章对你有帮助,欢迎点赞、收藏、关注,你的支持是我持续创作的动力!