【Java】封装位运算通用工具类——用一个整数字段替代几十个布尔列,极致节省存储空间

目录

一、前言:为什么要关注位运算?

二、位运算基础回顾

[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) 的结果可能是 248 等任何 2 的幂次,而不一定是 1

④ 取反(Toggle Bit)------ 翻转指定位

复制代码
原理:num ^ (1 << pos)

利用异或的特性:1 ^ 1 = 00 ^ 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.Collectionsjava.util.Arrays 等 JDK 工具类的设计理念一致。

updateBit方法的巧妙之处

updateBitsetBitclearBit 的统一封装。它的实现分两步:

  1. num & ~(1 << pos):先将目标位清零,无论它原来是 0 还是 1
  2. | (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 省略
}

六、进阶封装:模板方法模式

在实际项目中,位运算的使用往往遵循一个固定的流程:检查 → 翻转 → 持久化。比如:

  1. 检查用户是否已经领取过某个奖励(checkBit)
  2. 如果没有领取,将对应位置 1(setBit)
  3. 将更新后的值写回数据库(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 来表示布尔状态,用一个整数字段承载多个标记

回顾本文的设计要点:

  1. 两套工具类int 版本(31 个标记位)和 long 版本(63 个标记位),根据业务需求选择
  2. 枚举管理语义 :通过 枚举 + pos 字段 消除魔法数字,提升代码可读性和可维护性
  3. 模板方法封装:将「检查→翻转→持久化」的通用流程抽象为模板,子类只需关注业务逻辑
  4. 关键细节1L << pos 的必要性、符号位的规避、>>> 的使用、并发安全的考量

位运算不是银弹,它最适合的场景是:状态数量多、主要在应用层判断、以「只增不改」为主的标记类需求。在这类场景中,它能显著减少数据库列数、避免频繁的表结构变更,是一种值得掌握的实用技巧。


📌 如果这篇文章对你有帮助,欢迎点赞、收藏、关注,你的支持是我持续创作的动力!

相关推荐
xinhuanjieyi2 小时前
php给30支NBA球队添加logo图标,做好对应关系
android·开发语言·php
菜菜小狗的学习笔记2 小时前
八股(三)Java并发
java·开发语言
云烟成雨TD2 小时前
Spring AI Alibaba 1.x 系列【10】ReactAgent 工具加载和执行流程
java·人工智能·spring
lee_curry2 小时前
JUC第一章 java中基础概念和CompletableFuture
java·多线程·并发·juc
一晌小贪欢2 小时前
PyQt5 开发一个 PDF 批量合并工具
开发语言·qt·pdf
神仙别闹2 小时前
基于 MATLAB 实现的图像信号处理
开发语言·matlab·信号处理
迷藏4942 小时前
**超融合架构下的Go语言实践:从零搭建高性能容器化微服务集群**在现代云原生时代,*
java·python·云原生·架构·golang
swift192212 小时前
Qt多语言问题 —— 静态成员变量
开发语言·c++·qt
それども2 小时前
Spring Bean @Autowired自注入空指针问题
java·开发语言·spring