用了Stream后,代码反而越写越丑?

作者:微信小助手

发布时间:2025-04-01T10:04:26

兄弟们,前几天在技术群里看到一个令人窒息的代码片段:

List
     users = ...;
    
Map List > result = users.stream()
    .filter(u -> u.getStatus() == UserStatus.ACTIVE)
    .filter(u -> u.getAge() > 18)
    .filter(u -> u.getOrders().size() > 0)
    .flatMap(u -> u.getOrders().stream())
    .collect(Collectors.groupingBy(Order::getUserId,
        Collectors.mapping(Order::getId, Collectors.toList())));

这段代码乍一看很“流式”,但仔细分析就会发现:

  • 连续三个filter像俄罗斯套娃

  • flatMap后的collect嵌套层数堪比俄罗斯方块

  • 最终结果类型Map>与实际需求完全不符 

这让我想起最近流行的一句话:“Stream用得好是瑞士军刀,用不好就是小李飞刀——刀刀见血,但扎的是自己。”


一、Stream变丑的四大“毁容级”操作

1. 链式调用七龙珠

有些同学对链式调用有一种迷之执着,恨不得把所有操作塞进一条流里:

List
     result = 
    list.stream()
    .filter(...).map(...).flatMap(...).distinct()
    .sorted(...).limit(...).skip(...).collect(...);

危害:超过5个操作的链式调用会让代码可读性直线下降,调试时根本分不清问题出在哪一环。

解药

// 分段处理,用中间变量过渡
Stream<String> filtered = list.stream().filter(...);
Stream<String> mapped = filtered.map(...);
List<String> result = mapped.collect(...);

2. 滥用Collectors.joining

在某个电商项目里,我见过这样的代码:

String skuIds = order.getItems().stream()
    .map(Item::getSkuId)
    .collect(Collectors.joining(","));

问题:如果订单没有商品,会得到空字符串,而空字符串和null在业务逻辑中是不同的概念。 

改进方案

Optional
     skuIdsOpt = order.getItems().stream()
    
    .map(Item::getSkuId)
    .collect(Collectors.joining(","))
    .empty() ? Optional.empty() : Optional.of(skuIds);

3. 忽略性能的Collectors.toList()

某支付系统的关键路径上有这样的代码:

List
     transactions = db.query(...).stream()
    
    .filter(t -> t.getAmount() > 1000)
    .collect(Collectors.toList());

隐患:当数据量达到百万级时,toList()会导致内存溢出。 

优化方法

// 使用并行流+分批处理
List result =  new ArrayList<>();
db.query(...).stream()
    .parallel()
    .filter(t -> t.getAmount() > 1000)
    .forEachOrdered(result::add);

4. 错误使用boxed()

在某个金融计算场景中:

double sum = numbers.stream()
    .boxed()
    .mapToDouble(Number::doubleValue)
    .sum();

误区:boxed()会将原始类型转换为包装类型,导致性能下降。 

正确姿势

double sum = numbers.stream()
    .mapToDouble(Number::doubleValue)
    .sum();

二、Stream优雅编程的五大黄金法则

1. 保持流操作的原子性

反模式

List
     products = productList.stream()
    
    .filter(p -> p.getPrice() > 100)
    .filter(p -> p.getStock() > 0)
    .collect(Collectors.toList());

重构方案

// 定义独立的Predicate
Predicate isExpensive = p -> p.getPrice() >  100;
Predicate isInStock = p -> p.getStock() >  0;

List products = productList.stream()
    .filter(isExpensive.and(isInStock))
    .collect(Collectors.toList());

2. 合理使用peek()

在监控日志场景中:

List
     orders = orderList.stream()
    
    .peek(o -> log.info("Processing order: {}", o.getId()))
    .filter(o -> o.getStatus() == COMPLETED)
    .collect(Collectors.toList());

注意事项

  • peek()只能用于调试或副作用操作

  • 避免在peek中修改流元素

  • 并行流中慎用peek()

3. 自定义Collectors

在权限系统中,我们需要将用户角色收集为枚举集合:

public class RoleCollectors {
    public static Collector > rolesToSet() {
        return Collector.of(
            HashSet::new,
            (set, user) -> set.add(user.getRole()),
            (left, right) -> { left.addAll(right); return left; },
            Collector.Characteristics.UNORDERED
        );
    }
}

// 使用示例
Set roles = users.stream()
    .collect(RoleCollectors.rolesToSet());

4. 处理空流的正确姿势

在商品推荐场景中:

List
     recommendations = productList.stream()
    
    .filter(p -> p.getCategory().equals("electronics"))
    .findFirst()
    .map(Collections::singletonList)
    .orElse(Collections.emptyList());

更优雅的方式

List
     recommendations = productList.stream()
    
    .filter(p -> p.getCategory().equals("electronics"))
    .collect(Collectors.collectingAndThen(
        Collectors.toList(),
        list -> list.isEmpty() ? Collections.singletonList(defaultProduct) : list
    ));

5. 结合Optional使用

在用户服务中:

User user = userRepository.findById(userId)
    .orElseThrow(() -> new UserNotFoundException(userId));

return user.getEmail();

Stream优化版

return userRepository.findById(userId)
    .map(User::getEmail)
    .orElseThrow(() -> new UserNotFoundException(userId));

三、Stream高级技巧:让代码会“呼吸”

1. 分阶段处理流

在物流系统中,订单处理分为三个阶段:

Stream
     orderStream = orderRepository.findAll().stream();
    

// 阶段1:过滤有效订单
Stream validOrders = orderStream
    .filter(Order::isValid)
    .peek(o -> auditLogService.logValidation(o));

// 阶段2:计算运费
Map shippingFees = validOrders
    .collect(Collectors.toMap(
        Order::getId,
        this::calculateShippingFee
    ));

// 阶段3:更新订单状态
validOrders.forEach(o -> orderService.updateStatus(o, SHIPPING));

2. 使用Collectors.teeing()

在统计用户画像时:

UserProfile profile = users.stream()
    .collect(Collectors.teeing(
        Collectors.averagingInt(User::getAge),
        Collectors.summingDouble(User::getSpending),
        (avgAge, totalSpending) -> new UserProfile(avgAge, totalSpending)
    ));

3. 处理并行流的陷阱

在文件处理场景中:

List
     lines = Files.readAllLines(Paths.get(
    "data.txt"));

lines.parallelStream()
    .map(this::processLine)
    .forEachOrdered(System.out::println);

关键点

  • 顺序敏感操作必须使用forEachOrdered()

  • 共享资源需要线程安全处理

  • 避免过度使用并行流

4. 自定义Spliterator

在处理海量日志时:

public class LogSpliterator implements Spliterator<LogEntry{
    privatefinal List logs;
    privateint currentIndex;

    public LogSpliterator(List logs)  {
        this.logs = logs;
        this.currentIndex = 0;
    }

    @Override
    public boolean tryAdvance(Consumersuper LogEntry> action) {
        if (currentIndex < logs.size()) {
            action.accept(logs.get(currentIndex++));
            returntrue;
        }
        returnfalse;
    }

    @Override
    public Spliterator   trySplit() {
        int currentSize = logs.size() - currentIndex;
        if (currentSize < 1000returnnull// 阈值设定
        int splitPos = currentIndex + currentSize / 2;
        Spliterator split =  new LogSpliterator(logs.subList(currentIndex, splitPos));
        currentIndex = splitPos;
        return split;
    }

    @Override
    public long estimateSize() {
        return logs.size() - currentIndex;
    }

    @Override
    public int characteristics() {
        return Spliterator.ORDERED | Spliterator.SIZED | Spliterator.SUBSIZED;
    }
}

// 使用示例
StreamSupport.stream(new LogSpliterator(logs), true)
    .parallel()
    .forEach(this::processLog);

四、Stream代码质量检查清单

  1. 可读性检查

    1. 每个流操作是否有明确的业务含义

    2. 链式调用是否超过5层

    3. 是否使用了有意义的中间变量

  2. 性能检查

    1. 是否在不必要的情况下使用了boxed()

    2. 并行流是否适用于当前场景

    3. 收集操作是否考虑了数据规模

  3. 异常处理

    1. 是否处理了空流情况

    2. 是否合理使用了Optional

    3. 错误处理是否符合业务需求

  4. 可维护性

    1. 是否将复杂逻辑拆分为独立的方法

    2. 是否使用了自定义Collectors

    3. 是否考虑了未来的扩展需求


五、终极心法:流的本质是思维方式

最后分享一个真实案例:某电商平台通过重构Stream代码,使核心订单处理流程的代码量减少40%,同时性能提升30%。他们的秘诀在于:

  1. 将流操作与业务逻辑解耦

  2. 建立Stream最佳实践库

  3. 使用IDE插件(如StreamRefactor)进行代码检查 

记住:Stream不是代码竞赛的工具,而是提升代码质量的手段。当你开始思考如何让Stream代码更易读、易维护、易扩展时,你就真正掌握了它的精髓。