Java Stream API 无内置 JOIN,但可通过 flatMap/filter 模拟内连接、map/getOrDefault 实现左连接、groupingBy+flatMap 处理一对多左连接,并可封装为泛型工具方法。

Java 的 Stream API 本身不直接支持 SQL 风格的 内连接(INNER JOIN) 或 左连接(LEFT JOIN),但可以通过组合 flatMap、filter、collect 等操作,结合嵌套遍历与匹配逻辑,模拟出等价行为。关键在于把“连接”理解为:基于某个关联字段,将两个集合的元素按条件配对。
用 flatMap + filter 模拟内连接
内连接只保留左右集合中都能匹配上的记录。假设你有两个列表:users(用户)和 orders(订单),想按 userId 关联:
List<User> users = ...;
List<Order> orders = ...;
List<UserOrderPair> innerJoinResult = users.stream()
.flatMap(user -> orders.stream()
.filter(order -> Objects.equals(order.getUserId(), user.getId()))
.map(order -> new UserOrderPair(user, order)))
.collect(Collectors.toList());
说明:
当用户要求为 Java 代码添加注释、修改注释、审查注释、整理注释格式,或要求按照代码注释规范处理 Java 文件时,使用本技能。本技能用于统一生成和维护 Java 类注释模板、按 Git 对比 release 判断版本号、补充每行代码上方中文块注释、调整注释空行规则,并保持 Java 文件 UTF-8 无 BOM 编码。典型触发场景包括:补充类注释、更新 @version、检查 @author 来源、整理行尾注释、把注释放到代码上一行、检查注释是否符合规范、构建前检查 Java 注释风格。
-
flatMap对每个user展开所有匹配的order,相当于“笛卡尔积 + 条件过滤” - 匹配失败时,内部
stream().filter(...).map(...)产生空流,不会贡献任何元素 → 自然实现“仅保留匹配项” - 注意:时间复杂度是 O(m×n),大数据量时建议先将右集合转为
Map优化
用 map + getOrDefault 模拟左连接(推荐写法)
左连接要求保留左集合全部元素,右集合无匹配则补 null 或默认值。高效做法是预构建右集合的查找索引:
立即学习“Java免费学习笔记(深入)”;
// 构建订单按 userId 的映射(一个用户可能有多个订单,这里取第一个;如需全部,用 List<Order>)
Map<Long, Order> orderMap = orders.stream()
.collect(Collectors.toMap(Order::getUserId, Function.identity(), (a, b) -> a));
List<UserOrderPair> leftJoinResult = users.stream()
.map(user -> new UserOrderPair(user, orderMap.getOrDefault(user.getId(), null)))
.collect(Collectors.toList());
说明:
- 先用
Collectors.toMap建立userId → Order映射,冲突策略选保留第一个((a,b)->a) -
getOrDefault在查不到时返回null,符合左连接语义 - 若一个用户对应多个订单,需改为
Map<long list>></long>,再用flatMap展开
处理一对多关系的左连接(如用户→多个订单)
当左表一条记录对应右表多条时(如一个用户有多个订单),可这样写:
Map<Long, List<Order>> orderGroupMap = orders.stream()
.collect(Collectors.groupingBy(Order::getUserId));
List<UserOrderPair> leftJoinOneToMany = users.stream()
.flatMap(user -> {
List<Order> userOrders = orderGroupMap.getOrDefault(user.getId(), Collections.emptyList());
if (userOrders.isEmpty()) {
return Stream.of(new UserOrderPair(user, null));
} else {
return userOrders.stream().map(order -> new UserOrderPair(user, order));
}
})
.collect(Collectors.toList());
说明:
-
groupingBy将订单按userId分组,生成Map<long list>></long> - 对每个用户,若无订单,生成一个
null订单的配对;若有,则为每个订单生成一个配对 - 结果数量 = 左表总数 + 右表匹配总数,符合一对多左连接语义
封装成通用工具方法(提升复用性)
可以抽象出泛型方法,减少重复代码:
public static <L, R, K> Stream<Pair<L, R>> innerJoin(
Stream<L> left,
Stream<R> right,
Function<L, K> leftKey,
Function<R, K> rightKey) {
Map<K, List<R>> rightMap = right.collect(Collectors.groupingBy(rightKey));
return left.flatMap(l ->
rightMap.getOrDefault(leftKey.apply(l), Collections.emptyList())
.stream()
.map(r -> new Pair<>(l, r)));
}
// 使用示例
List<Pair<User, Order>> result = innerJoin(
users.stream(),
orders.stream(),
User::getId,
Order::getUserId)
.collect(Collectors.toList());
说明:
- 参数化左右流、键提取函数,适配任意类型
- 内部仍采用分组映射 + flatMap,兼顾可读性与效率
-
Pair可用AbstractMap.SimpleEntry、Tuples(Vavr)或自定义简单类
Stream 连接不是语法糖,而是思维方式的转换:它不维护状态、不建临时表,靠函数组合表达逻辑。只要理清“匹配”“补空”“展开”三个动作,就能写出清晰可靠的连接逻辑。

















