高效处理Long列表与集合运算:基于RoaringBitmap的工具类解析与应用场景

16 阅读8分钟
<dependency>
    <groupId>org.roaringbitmap</groupId>
    <artifactId>RoaringBitmap</artifactId>
    <version>0.9.23</version> <!-- 建议使用最新稳定版本 -->
</dependency>

一、工具类概览

该工具类提供了将List<Long>转换为RoaringBitmap的多种方式,并内置了集合逻辑运算方法。核心目标:让开发者用几行代码完成Long集合的高效存储与运算


二、方法详解与使用场景

1. eachLongToBitmap / eachLongToBitmapStream

代码作用:遍历输入列表,为每一个Long值单独创建一个RoaringBitmap,每个bitmap中只包含该值。

java

public static List<RoaringBitmap> eachLongToBitmap(List<Long> longList)
public static List<RoaringBitmap> eachLongToBitmapStream(List<Long> longList) // Stream版本

📌 使用场景

  • 文档-词项倒排索引:假设每个文档ID对应多个关键词,但你需要反向记录每个关键词出现在哪些文档的哪个位置。如果位置信息用独立bitmap存储,可以快速定位“关键词X在第3个位置出现”的所有文档。
  • 用户行为序列的事件标记:在点击流分析中,每个用户ID对应一组带时间戳的行为。如果你需要独立追踪每个行为发生的顺序(例如“第1次点击商品A”),可以为每个行为序号创建独立的bitmap,从而高效查询“完成过第5次点击的用户集合”。
  • 分布式ID分片校验:在分布式系统中,有时需要验证一批ID是否属于某个分片。将每个ID单独转为bitmap后,可以与分片的bitmap进行快速成员测试(contains)。

⚠️ 注意:此方式会生成N个bitmap对象,内存开销随列表长度线性增长,仅适合列表长度较小(如<1000)或对“每个元素独立操作”有强需求的场景。

2. allLongsToOneBitmap / allLongsToOneBitmapStream

代码作用:将整个List<Long>中的所有值合并到一个RoaringBitmap中,返回仅包含这一个bitmap的列表。

java

public static List<RoaringBitmap> allLongsToOneBitmap(List<Long> longList)
public static List<RoaringBitmap> allLongsToOneBitmapStream(List<Long> longList) // Stream版本

📌 使用场景(最常见)

  • 用户标签系统:电商平台有“高活跃用户”、“高消费用户”、“偏好电子产品用户”三个标签。每个标签对应一个包含几十万用户ID的List<Long>,用allLongsToOneBitmap分别生成三个bitmap,然后可以秒级计算“高活跃 ∩ 高消费 ∩ 电子产品”的精准营销人群。
  • 黑白名单过滤:风控系统中,黑名单ID集合通常很大(千万级)。将其加载为一个RoaringBitmap,判断一个ID是否在黑名单中仅需O(1)时间,且内存仅为HashSet的1/10左右。
  • 离线数据同步时的集合差集:每日从Hive中导出一批需要推送的用户ID(增量),与昨日已推送的bitmap做andNot,只处理新增用户。整个过程无需遍历,直接位运算完成。

✅ 推荐:绝大多数“集合存储与运算”场景应优先采用此方式。

3. computeLogic 私有方法

代码作用:对传入的多个RoaringBitmap执行AND(交集)或OR(并集)运算,返回结果bitmap。

java

private static RoaringBitmap computeLogic(List<RoaringBitmap> bitmaps, String logic)

📌 使用场景

  • 多条件组合筛选:招聘平台中,职位搜索条件为“学历=本科 AND 经验=3~5年 AND 城市=北京”。每个条件对应一个候选职位ID的bitmap,通过computeLogic(bitmaps, "AND")直接获得最终职位列表。
  • A/B实验流量划分:实验平台需要将用户分为“对照组”和“实验组”。两组用户分别对应两个bitmap,当需要“实验组且未在对照组中”的用户时,使用AND结合andNot。而需要“实验组或对照组”的总人群时,使用OR
  • 动态权限聚合:一个用户属于多个角色,每个角色有一组可访问的资源ID(bitmap)。最终用户的权限 = 各角色资源的OR;若要实现“必须具备所有角色才能访问”的安全策略,则使用AND

三、集合运算示例与实际应用场景

main方法中演示了交、并、差、对称差等典型运算。我将为每个运算补充更具象的业务案例。

1. 交集(AND)—— RoaringBitmap.and(bitmapA, bitmapB)

业务场景精准营销人群圈选
某APP想给“过去7天登录过”且“在购物车中添加过商品”的用户推送优惠券。

  • 集合A:登录用户ID
  • 集合B:加购用户ID
    交集结果即为目标人群。

2. 并集(OR)—— RoaringBitmap.or(bitmapA, bitmapB)

业务场景合并多渠道用户来源
市场部门需要统计“通过抖音广告”或“通过百度搜索”进入过落地页的用户总数(去重)。

  • 集合A:抖音渠道用户
  • 集合B:百度渠道用户
    并集给出独立用户总数。

3. 差集(AND NOT)—— RoaringBitmap.andNot(bitmapA, bitmapB)

业务场景排除已推送过的用户
运营每天要发push,但不能重复推送给昨天已收到push的用户。

  • 集合A:今天符合条件的用户
  • 集合B:昨天已推送的用户
    差集 A - B 得到“今天应推送但昨天未推送”的用户。

4. 对称差(XOR)—— RoaringBitmap.xor(bitmapA, bitmapB)

业务场景好友推荐中的“双向未关注”
社交平台:A的好友列表为集合A,B的好友列表为集合B。对称差包含“在A中但不在B中”以及“在B中但不在A中”的人,即双方尚未建立好友关系的候选推荐对象。

5. 多Bitmap逻辑组合(computeLogic)

业务场景高级筛选器
电商后台:商家需要筛选出“价格在100~200元”  (“品牌为小米”  “品牌为华为”)的商品。

  • 生成三个bitmap:priceBitmapxiaomiBitmaphuaweiBitmap
  • 先用computeLogic对小米和华为做OR,再与priceBitmapAND

四、总结与最佳实践

转换方式内存占用适用数据量典型场景
eachLongToBitmap高(N个bitmap对象)小(N<1000)位置索引、独立事件标记
allLongsToOneBitmap低(1个bitmap)极大(千万级)用户画像、黑白名单、集合运算

额外建议

  • 若原始Long值可能超过int范围(> 2^31-1),RoaringBitmap无法直接存储。实际业务中用户ID、商品ID通常控制在int范围内,否则需采用Roaring64NavigableMap
  • 对于Stream版本,小列表可提升可读性,大列表推荐使用传统循环以避免装箱开销。
  • 集合运算尽量使用RoaringBitmap提供的静态方法(如andorxorandNot),它们内部经过高度优化,比手动循环快数倍。

通过合理运用RoaringBitmap,你可以在不引入重型计算引擎(如Spark、Flink)的情况下,在单机内存中轻松处理亿级ID的集合运算。希望这篇AI生成的文章能帮助你在实际项目中选对工具、用对场景。

java 代码 示例

public class LongListToBitmapList {

    /**
     * 方式一:每个 Long 值生成一个独立的 RoaringBitmap
     * @param longList 原始 Long 列表
     * @return List<RoaringBitmap> 每个 bitmap 包含对应位置的一个 long 值
     */
    public static List<RoaringBitmap> eachLongToBitmap(List<Long> longList) {
        List<RoaringBitmap> bitmapList = new ArrayList<>();
        for (Long value : longList) {
            RoaringBitmap bitmap = new RoaringBitmap();
            // RoaringBitmap 存储的是 int 范围内的无符号整数,long 需要转成 int(实际场景确保值不超过 int 范围)
            bitmap.add(value.intValue());
            bitmapList.add(bitmap);
        }
        return bitmapList;
    }

    /**
     * 方式二:将所有 Long 值合并到一个 RoaringBitmap 中,然后返回仅含该 bitmap 的列表
     * @param longList 原始 Long 列表
     * @return List<RoaringBitmap> 包含一个聚合了所有值的 bitmap
     */
    public static List<RoaringBitmap> allLongsToOneBitmap(List<Long> longList) {
        RoaringBitmap bitmap = new RoaringBitmap();
        for (Long value : longList) {
            bitmap.add(value.intValue());
        }
        List<RoaringBitmap> result = new ArrayList<>();
        result.add(bitmap);
        return result;
    }

    // 使用 Java Stream 的简洁写法(方式一)
    public static List<RoaringBitmap> eachLongToBitmapStream(List<Long> longList) {
        return longList.stream()
                .map(value -> {
                    RoaringBitmap bitmap = new RoaringBitmap();
                    bitmap.add(value.intValue());
                    return bitmap;
                })
                .collect(Collectors.toList());
    }

    // 使用 Java Stream 的简洁写法(方式二)
    public static List<RoaringBitmap> allLongsToOneBitmapStream(List<Long> longList) {
        RoaringBitmap bitmap = new RoaringBitmap();
        longList.forEach(value -> bitmap.add(value.intValue()));
        return Arrays.asList(bitmap);
    }

    /**
     * 对多个 RoaringBitmap 执行交(AND)或并(OR)运算
     * @param bitmaps 参与运算的 bitmap 列表
     * @param logic   "AND" 或 "OR"(大小写不敏感)
     * @return 运算结果
     */
    private static RoaringBitmap computeLogic(List<RoaringBitmap> bitmaps, String logic) {
        if (bitmaps == null || bitmaps.isEmpty()) {
            return new RoaringBitmap();
        }
        RoaringBitmap result = new RoaringBitmap();
        if ("AND".equalsIgnoreCase(logic)) {
            // 先用第一个 bitmap 作为初始值,再与其余做 and
            result.or(bitmaps.get(0));
            for (int i = 1; i < bitmaps.size(); i++) {
                result.and(bitmaps.get(i));
            }
        } else if ("OR".equalsIgnoreCase(logic)) {
            bitmaps.forEach(result::or);
        }
        return result;
    }

    // 测试示例(包含交、并、差)
    public static void main(String[] args) {
        // 准备两个 Long 列表,分别代表两个集合
        List<Long> listA = Arrays.asList(1L, 2L, 3L, 4L, 5L);
        List<Long> listB = Arrays.asList(4L, 5L, 6L, 7L, 8L);

        // 转换为独立的 bitmap(每个元素一个 bitmap,用于演示多 bitmap 交并)
        // 实际业务中往往每个集合对应一个 bitmap(包含多个元素),这里我们演示两种风格:
        // 风格1:每个集合合并为一个 bitmap(更常见)
        RoaringBitmap bitmapA = new RoaringBitmap();
        listA.forEach(v -> bitmapA.add(v.intValue()));
        RoaringBitmap bitmapB = new RoaringBitmap();
        listB.forEach(v -> bitmapB.add(v.intValue()));

        System.out.println("集合 A: " + listA);
        System.out.println("集合 B: " + listB);

        // 1. 交集 (AND)
        RoaringBitmap intersection = RoaringBitmap.and(bitmapA, bitmapB);
        System.out.println("交集 (A ∩ B): " + intersection);

        // 2. 并集 (OR)
        RoaringBitmap union = RoaringBitmap.or(bitmapA, bitmapB);
        System.out.println("并集 (A ∪ B): " + union);

        // 3. 差集 (A - B,即 A AND NOT B)
        RoaringBitmap differenceAB = RoaringBitmap.andNot(bitmapA, bitmapB);
        System.out.println("差集 (A - B): " + differenceAB);

        // 4. 差集 (B - A)
        RoaringBitmap differenceBA = RoaringBitmap.andNot(bitmapB, bitmapA);
        System.out.println("差集 (B - A): " + differenceBA);

        // 5. 对称差 (XOR) 即 (A - B) ∪ (B - A)
        RoaringBitmap xor = RoaringBitmap.xor(bitmapA, bitmapB);
        System.out.println("对称差 (A ⊕ B): " + xor);

        // 6. 演示 computeLogic 方法(支持多个 bitmap)
        List<RoaringBitmap> multiBitmaps = Arrays.asList(bitmapA, bitmapB);
        RoaringBitmap andResult = computeLogic(multiBitmaps, "AND");
        RoaringBitmap orResult = computeLogic(multiBitmaps, "OR");
        System.out.println("computeLogic AND: " + andResult);
        System.out.println("computeLogic OR : " + orResult);
    }
    
-------------结果如下-------------   
集合 A: [1, 2, 3, 4, 5]
集合 B: [4, 5, 6, 7, 8]
交集 (A ∩ B): {4,5}
并集 (A ∪ B): {1,2,3,4,5,6,7,8}
差集 (A - B): {1,2,3}
差集 (B - A): {6,7,8}
对称差 (A ⊕ B): {1,2,3,6,7,8}
computeLogic AND: {4,5}
computeLogic OR : {1,2,3,4,5,6,7,8}