Java递归调用无法保留集合值?递归方法问题排查与实现方案
问题分析
首先给个明确结论:如果你的产品层级是严格的树结构(没有循环引用、没有一个子产品归属多个父类的情况),你现在写的代码返回的结果是正确的,不存在递归返回值丢失、元素存不住的问题。但它有几个非常严重的缺陷,只要数据量上去或者数据出现异常,直接出故障:
- 性能浪费极其严重
每一层递归都要新建一个独立的HashSet,递归返回时还要把下层所有返回的UUID全量复制到当前层的集合里,整体时间复杂度是O(depth * n),depth是树的最大深度,n是总产品数。如果产品分类层级深、总数多,会产生大量无意义的集合创建、对象拷贝操作,平白消耗CPU和内存。
另外你在循环里已经手动add了子产品的UUID,但是递归调用的第一行又会把同一个UUID加入下层方法新建的集合,最后再被addAll回当前层,属于完全多余的重复操作,虽然HashSet去重不影响结果,但完全没必要。 - 没有防环逻辑,极易栈溢出
代码没有任何已访问节点的标记,一旦数据出现异常(比如配错了父子关系,A的子类是B,B的子类又设成A;或者某个产品的父类设成了自己),递归会无限死循环,直接抛StackOverflowError打挂服务。就算没有环,如果产品结构是图(一个子产品属于多个父类),同一个节点会被重复访问、重复查库,性能会进一步劣化。 - 递归层重复查库,IO压力大
每进一次递归就跑一次数据库查询查子类,总查询次数等于访问到的总节点数,节点多的时候数据库压力会非常大,接口响应时间会飙升。
修正方案
核心思路是所有递归层级共享同一个去重集合,不用每层新建集合、来回拷贝元素,同时用这个集合直接做已访问标记,从根源上解决重复访问、循环递归的问题。
基础优化版(改造成本最低,兼容你现有DAO逻辑)
把存储结果的Set作为参数透传到所有递归层,所有操作都在同一个集合上执行,不需要每层返回集合:
// 对外暴露的公共入口方法 public List<UUID> recursiveMethod(UUID productUuid) { Set<UUID> visitedSet = new HashSet<>(); collectAllSubUuids(productUuid, visitedSet); return new ArrayList<>(visitedSet); } // 内部私有递归方法,不对外暴露 private void collectAllSubUuids(UUID currentUuid, Set<UUID> visitedSet) { // add方法返回false说明这个UUID已经处理过,直接跳过,天然防环、防重复访问 if (!visitedSet.add(currentUuid)) { return; } List<Product> subProducts = productRepo.getSubProducts(currentUuid); for (Product subProduct : subProducts) { collectAllSubUuids(subProduct.getProductUuid(), visitedSet); } }
这个版本每个节点只会被处理一次、查询一次,没有多余的集合拷贝,时间复杂度直接降到O(n),同时自动处理了循环引用、多父节点的异常场景,不会出现无限递归。
进阶优化版(性能更高,适合数据量较大的场景)
如果产品总数不算特别大,建议一次性把所有产品的父子关联关系从数据库查出来,在内存里做递归遍历,完全避免递归过程中的数据库IO,性能会有数量级的提升:
public List<UUID> recursiveMethod(UUID productUuid) { // 一次性查全量父子关系,返回值是父UUID对应所有子UUID的映射 Map<UUID, List<UUID>> parentSubMap = productRepo.getAllParentSubRelations(); Set<UUID> visitedSet = new HashSet<>(); collectAllSubUuids(productUuid, parentSubMap, visitedSet); return new ArrayList<>(visitedSet); } private void collectAllSubUuids(UUID currentUuid, Map<UUID, List<UUID>> relationMap, Set<UUID> visitedSet) { if (!visitedSet.add(currentUuid)) { return; } List<UUID> subUuids = relationMap.getOrDefault(currentUuid, Collections.emptyList()); for (UUID subUuid : subUuids) { collectAllSubUuids(subUuid, relationMap, visitedSet); } }
如果你的产品分类层级极深(比如超过1000层),即使没有环也可能触发栈溢出,这种场景可以把递归改成用显式
Stack或者队列的广度优先遍历实现,彻底规避栈溢出风险。
内容的提问来源于stack exchange,提问作者user19254373
相关产品推荐
相关产品推荐

