函数式编程思想,Java 8-Lambda 小结

0 阅读11分钟

你想了解如何在 Java 中应用函数式编程思想,这是一个非常好的进阶方向,Java 从 8 版本开始引入了 Lambda 表达式、Stream API 等核心特性,让函数式编程变得非常便捷。

一、函数式编程核心思想(Java 视角)

函数式编程的核心是将函数作为一等公民,强调:

  1. 无副作用(函数执行不修改外部状态)
  2. 不可变数据(避免修改已有数据,而是生成新数据)
  3. 声明式编程(关注 “做什么” 而非 “怎么做”)
  4. 纯函数(相同输入永远返回相同输出)

二、Java 中函数式编程的核心工具

1. 函数式接口(Functional Interface)

Java 中通过函数式接口(只有一个抽象方法的接口)来表示 “函数”,JDK 内置了常用的函数式接口:

  • Consumer<T>:消费型(输入 T,无返回值)
  • Supplier<T>:供给型(无输入,返回 T)
  • Function<T, R>:函数型(输入 T,返回 R)
  • Predicate<T>:断言型(输入 T,返回 boolean)

2. Lambda 表达式(函数的简洁表示)

Lambda 是函数式接口的 “语法糖”,用来简化匿名内部类的写法。

3. Stream API(函数式数据处理)

Stream 是对集合数据的函数式操作流,支持过滤、映射、聚合等操作,全程无副作用。

三、Java 函数式编程实战示例

示例 1:基础 Lambda + 函数式接口

java

运行

import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;

public class FunctionalBasic {
    public static void main(String[] args) {
        // 1. Supplier:供给型(无参返回)
        Supplier<String> helloSupplier = () -> "Hello, 函数式编程";
        System.out.println(helloSupplier.get()); // 输出:Hello, 函数式编程

        // 2. Consumer:消费型(有参无返回)
        Consumer<String> printConsumer = (msg) -> System.out.println("消费:" + msg);
        printConsumer.accept(helloSupplier.get()); // 输出:消费:Hello, 函数式编程

        // 3. Function:函数型(入参→出参)
        Function<String, Integer> lengthFunction = (str) -> str.length();
        int length = lengthFunction.apply("Java Functional");
        System.out.println("字符串长度:" + length); // 输出:16

        // 4. Predicate:断言型(入参→布尔)
        Predicate<Integer> isEven = (num) -> num % 2 == 0;
        System.out.println("是否偶数:" + isEven.test(length)); // 输出:true
    }
}

示例 2:Stream API 实战(核心场景)

假设我们有一个用户列表,需要完成:过滤成年用户 → 提取用户名 → 按字母排序 → 输出。

java

运行

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

// 定义用户类
class User {
    private String name;
    private int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // getter
    public String getName() { return name; }
    public int getAge() { return age; }
}

public class StreamExample {
    public static void main(String[] args) {
        List<User> users = Arrays.asList(
                new User("Alice", 25),
                new User("Bob", 17),
                new User("Charlie", 30),
                new User("David", 19)
        );

        // 函数式写法(声明式,关注“做什么”)
        List<String> adultUserNames = users.stream()
                .filter(user -> user.getAge() >= 18) // 过滤成年用户(Predicate)
                .map(User::getName) // 提取用户名(Function,方法引用简化)
                .sorted() // 排序
                .collect(Collectors.toList()); // 收集结果

        // 输出结果:[Alice, Charlie, David]
        System.out.println(adultUserNames);

        // 对比传统命令式写法(关注“怎么做”,代码更繁琐)
        /*
        List<String> result = new ArrayList<>();
        for (User user : users) {
            if (user.getAge() >= 18) {
                result.add(user.getName());
            }
        }
        Collections.sort(result);
        System.out.println(result);
        */
    }
}

示例 3:自定义函数式接口

如果 JDK 内置的接口不满足需求,可以自定义函数式接口(用 @FunctionalInterface 注解标记):

java

运行

@FunctionalInterface // 注解可选,但推荐加(编译器会检查是否符合函数式接口规范)
interface Calculator<T> {
    T calculate(T a, T b);
}

public class CustomFunctionalInterface {
    public static void main(String[] args) {
        // 实现加法
        Calculator<Integer> add = (a, b) -> a + b;
        System.out.println("加法:" + add.calculate(10, 20)); // 30

        // 实现乘法
        Calculator<Integer> multiply = (a, b) -> a * b;
        System.out.println("乘法:" + multiply.calculate(10, 20)); // 200
    }
}

四、Java 函数式编程的最佳实践

  1. 优先使用不可变集合:用 Collections.unmodifiableList() 或 Guava 的不可变集合,避免外部修改。
  2. 避免在 Lambda 中修改外部变量:Lambda 中引用的外部变量必须是 final 或 Effectively final(实际不可变)。
  3. 使用方法引用简化代码:如 User::getName 比 user -> user.getName() 更简洁。
  4. Stream 延迟执行:Stream 的中间操作(filter、map)是延迟的,只有终端操作(collect、forEach)才会触发执行。
  5. 避免过度使用 Stream:简单的循环(如单步遍历)用传统 for 循环更易读,复杂数据处理用 Stream 更优雅。

总结

  1. Java 函数式编程的核心是函数式接口 + Lambda 表达式 + Stream API,从 Java 8 开始支持。
  2. 函数式编程强调声明式、无副作用、不可变,相比传统命令式编程,代码更简洁、易维护(尤其适合数据处理)。
  3. 核心场景:集合数据过滤 / 映射 / 聚合(Stream API)、回调函数(Lambda)、简化匿名内部类。

如果需要针对某个具体场景(比如并行流、Optional 空值处理、CompletableFuture 异步编程)深入讲解,可以告诉我。

如何在 Java 中使用 Stream API 进行复杂的数据处理?

你想掌握在 Java 中使用 Stream API 处理复杂数据场景的方法,这是函数式编程的核心应用场景。Stream API 不仅能处理简单的过滤 / 映射,还能应对分组、聚合、嵌套集合处理、多条件筛选等复杂场景,我会结合实战案例带你系统掌握。

一、复杂数据处理的核心能力(Stream 关键操作)

先明确 Stream 处理复杂数据的核心操作分类,后续案例会基于这些操作组合:

操作类型核心方法作用
中间操作filter/map/flatMap/sorted/distinct数据筛选、转换、扁平化、排序、去重
聚合操作reduce/collect汇总、归约、收集(核心,处理复杂场景)
分组 / 分区Collectors.groupingBy/partitioningBy按条件分组 / 二分分区
统计聚合Collectors.summingInt/averagingLong求和、平均值、最大值 / 最小值等统计计算
嵌套处理flatMap处理嵌套集合(如 List)

二、复杂场景实战案例

以下案例基于一个订单系统的真实场景(包含订单、订单项、用户维度),先定义基础数据模型:

java

运行

import lombok.AllArgsConstructor;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;

// 用户模型
@Data
@AllArgsConstructor
class User {
    private Long userId;       // 用户ID
    private String userName;   // 用户名
    private String city;       // 所在城市
}

// 订单项模型
@Data
@AllArgsConstructor
class OrderItem {
    private Long productId;    // 商品ID
    private String productName;// 商品名称
    private Integer quantity;  // 数量
    private BigDecimal price;  // 单价
}

// 订单模型
@Data
@AllArgsConstructor
class Order {
    private Long orderId;      // 订单ID
    private Long userId;       // 关联用户ID
    private LocalDateTime createTime; // 创建时间
    private List<OrderItem> items;    // 订单项列表
    private BigDecimal totalAmount;   // 订单总金额
}

准备测试数据(模拟业务场景的复杂数据集):

java

运行

// 初始化测试数据
User user1 = new User(1L, "张三", "北京");
User user2 = new User(2L, "李四", "上海");
User user3 = new User(3L, "王五", "北京");

OrderItem item1 = new OrderItem(101L, "手机", 1, new BigDecimal("5999"));
OrderItem item2 = new OrderItem(102L, "耳机", 2, new BigDecimal("299"));
OrderItem item3 = new OrderItem(201L, "电脑", 1, new BigDecimal("8999"));
OrderItem item4 = new OrderItem(202L, "鼠标", 1, new BigDecimal("99"));
OrderItem item5 = new OrderItem(301L, "键盘", 2, new BigDecimal("199"));

Order order1 = new Order(1001L, 1L, 
  LocalDateTime.of(2026, 1, 10, 10, 0),
  List.of(item1, item2), new BigDecimal("6597"));
Order order2 = new Order(1002L, 1L, 
  LocalDateTime.of(2026, 1, 15, 14, 0), 
  List.of(item3), new BigDecimal("8999"));
Order order3 = new Order(1003L, 2L, 
  LocalDateTime.of(2026, 1, 12, 9, 0), 
  List.of(item4, item5), new BigDecimal("497"));
Order order4 = new Order(1004L, 3L, 
  LocalDateTime.of(2026, 1, 20, 16, 0), 
  List.of(item1), new BigDecimal("5999"));

List<User> userList = List.of(user1, user2, user3);
List<Order> orderList = List.of(order1, order2, order3, order4);


场景 1:多条件筛选 + 数据转换(基础复杂筛选)

需求:筛选出 2026 年 1 月 10 日后创建、总金额 > 5000 且所属用户在北京的订单,最终只保留订单 ID 和用户名。

java

运行

import java.util.stream.Collectors;

// 步骤:1. 关联用户和订单 2. 多条件筛选 3. 转换结果格式
List<String> result1 = orderList.stream()
        // 关联用户(通过userId匹配)
        .map(order -> {
            User user = userList.stream()
                    .filter(u -> u.getUserId().equals(order.getUserId()))
                    .findFirst()
                    .orElse(null);
            return new Object() { // 匿名对象临时存储关联结果
                Long orderId = order.getOrderId();
                String userName = user != null ? user.getUserName() : "未知";
                String city = user != null ? user.getCity() : "";
                LocalDateTime createTime = order.getCreateTime();
                BigDecimal totalAmount = order.getTotalAmount();
            };
        })
        // 多条件筛选:北京用户 + 1月10日后 + 金额>5000
        .filter(temp -> "北京".equals(temp.city)
                && temp.createTime.isAfter(LocalDateTime.of(2026, 1, 10, 0, 0))
                && temp.totalAmount.compareTo(new BigDecimal("5000")) > 0)
        // 转换结果格式:"订单ID-用户名"
        .map(temp -> temp.orderId + "-" + temp.userName)
        .collect(Collectors.toList());

System.out.println(result1); // 输出:[1002-张三, 1004-王五]

关键解释

  • map 不仅能简单转换类型,还能做数据关联(这里关联订单和用户);
  • 多条件 filter 可以组合任意布尔逻辑,BigDecimal 比较要用 compareTo 而非 >/<
  • 匿名对象用于临时存储多字段关联结果(也可以自定义 DTO 类,更规范)。

场景 2:分组聚合(核心复杂场景)

需求:按用户所在城市分组,统计每个城市的:1. 订单总数 2. 订单总金额 3. 平均订单金额。

java

运行

import java.math.BigDecimal;
import java.util.Map;
import java.util.stream.Collectors;

// 步骤:1. 关联订单和城市 2. 按城市分组 3. 聚合统计
Map<String, Object> result2 = orderList.stream()
        // 关联订单和城市
        .map(order -> {
            String city = userList.stream()
                    .filter(u -> u.getUserId().equals(order.getUserId()))
                    .map(User::getCity)
                    .findFirst()
                    .orElse("未知");
            return new Object() {
                String city = city;
                BigDecimal totalAmount = order.getTotalAmount();
            };
        })
        // 按城市分组,并聚合统计(Collectors.teeing 是Java 12+特性,支持多聚合)
        .collect(Collectors.groupingBy(
                temp -> temp.city, // 分组键:城市
                Collectors.teeing(
                        Collectors.counting(), // 聚合1:订单总数
                        Collectors.reducing(BigDecimal.ZERO,
                             temp -> temp.totalAmount, BigDecimal::add), 
                        // 聚合2:总金额
                        (count, total) -> new Object() { // 合并两个聚合结果
                            long orderCount = count;
                            BigDecimal totalAmount = total;
                            BigDecimal avgAmount = total
                                    .divide(BigDecimal.valueOf(count),
                                     2, BigDecimal.ROUND_HALF_UP); 
                            // 平均金额(保留2位)
                        }
                )
        ));

// 输出结果
result2.forEach((city, stats) -> {
    System.out.println("城市:" + city);
    System.out.println("  - 订单总数:" + stats.orderCount);
    System.out.println("  - 总金额:" + stats.totalAmount);
    System.out.println("  - 平均金额:" + stats.avgAmount);
});
/* 输出:
城市:北京
  - 订单总数:3
  - 总金额:21595
  - 平均金额:7198.33
城市:上海
  - 订单总数:1
  - 总金额:497
  - 平均金额:497.00
*/

关键解释

  • Collectors.groupingBy 是分组核心,第一个参数是分组键,第二个参数是聚合逻辑;
  • Collectors.teeing 解决多维度聚合问题(一次遍历完成多个统计),避免多次流操作;
  • Collectors.reducing 用于自定义归约(这里实现 BigDecimal 求和,因为 JDK 内置的 summingBigDecimal 是 Java 8 后续版本才支持)。

场景 3:嵌套集合扁平化处理

需求:提取所有订单中的商品,去重后统计每个商品的总销售数量和总销售额。

java

运行

import java.util.Map;
import java.util.stream.Collectors;

// 步骤:1. 扁平化订单项(List<Order> → List<OrderItem>)
// 2. 按商品分组 3. 统计数量和金额
Map<String, Object> result3 = orderList.stream()
        // flatMap 扁平化嵌套集合:将每个订单的items列表展开为单个item流
        .flatMap(order -> order.getItems().stream())
        // 按商品名称分组,聚合统计
        .collect(Collectors.groupingBy(
                OrderItem::getProductName, // 分组键:商品名称
                Collectors.teeing(
                        // 聚合1:总销售数量(求和quantity)
                        Collectors.summingInt(OrderItem::getQuantity),
                        // 聚合2:总销售额(quantity * price 求和)
                        Collectors.reducing(
                                BigDecimal.ZERO,
                                item -> item.getPrice()
                                .multiply(BigDecimal.valueOf(item.getQuantity())),
                                BigDecimal::add
                        ),
                        // 合并结果
                        (totalQty, totalSales) -> new Object() {
                            int totalQuantity = totalQty;
                            BigDecimal totalSalesAmount = totalSales;
                        }
                )
        ));

// 输出结果
result3.forEach((product, stats) -> {
    System.out.println("商品:" + product);
    System.out.println("  - 总销量:" + stats.totalQuantity);
    System.out.println("  - 总销售额:" + stats.totalSalesAmount);
});
/* 输出:
商品:手机
  - 总销量:2
  - 总销售额:11998
商品:耳机
  - 总销量:2
  - 总销售额:598
商品:电脑
  - 总销量:1
  - 总销售额:8999
商品:鼠标
  - 总销量:1
  - 总销售额:99
商品:键盘
  - 总销量:2
  - 总销售额:398
*/

关键解释

  • flatMap 是处理嵌套集合的核心(如 List<Order> 中每个 Order 包含 List<OrderItem>),能将 “流的流”(Stream<Stream<OrderItem>>)转换为 “单一流”(Stream<OrderItem>);
  • 聚合时可以结合基础类型求和(summingInt)和 BigDecimal 自定义求和,覆盖不同数据类型的统计需求。

场景 4:排序 + 限制结果 + 归约

需求:找出 2026 年 1 月创建的订单中,总金额最高的前 2 个订单,计算它们的金额总和。

java

运行

import java.math.BigDecimal;
import java.time.Month;
import java.util.Comparator;

BigDecimal result4 = orderList.stream()
        // 筛选:2026年1月创建的订单
        .filter(order -> order.getCreateTime().getYear() == 2026 
                && order.getCreateTime().getMonth() == Month.JANUARY)
        // 按总金额降序排序
        .sorted(Comparator.comparing(Order::getTotalAmount).reversed())
        // 限制前2个
        .limit(2)
        // 归约求和:将前2个订单的金额相加
        .map(Order::getTotalAmount)
        .reduce(BigDecimal.ZERO, BigDecimal::add);

System.out.println("前2高金额订单总和:" + result4); 
// 输出:15998(8999+5999)

关键解释

  • sorted 结合 Comparator.comparing 实现自定义排序(reversed() 降序);
  • limit(n) 截取前 n 条结果,常和排序配合实现 “Top N” 需求;
  • reduce 是基础归约操作,第二个参数是二元操作符(这里用 BigDecimal 的 add 方法)。

三、复杂 Stream 处理的最佳实践

  1. 避免链式过长:如果 Stream 操作超过 5 步,拆分为多个小流或提取为方法(如单独写一个getCityByUserId方法),提升可读性;

  2. 优先使用并行流(谨慎)  :数据量超大时(万级以上),用 parallelStream() 替代 stream() 提升性能,但要确保:

    • 无状态操作(filter/map/flatMap 是无状态,sorted/limit 是有状态);
    • 无线程安全问题(不修改外部变量);
  3. 空值处理:用 Optional 避免空指针,比如 findFirst().orElse(null) 改为 findFirst().orElseThrow(() -> new RuntimeException("用户不存在"))(根据业务选择);

  4. 避免重复流操作:同一个集合不要多次创建 Stream,可先收集为中间集合(如 List)复用;

  5. 数据量大时慎用 collect:如果只是遍历输出,直接用 forEach 而非 collect 后再遍历,减少内存占用。

总结

  1. Stream API 处理复杂数据的核心是操作组合filter/map/flatMap 做数据预处理,groupingBy/teeing/reducing 做聚合统计,sorted/limit 做排序截取;
  2. flatMap 是处理嵌套集合的关键,Collectors.teeing 是多维度聚合的最优解(Java 12+),groupingBy 是分组统计的核心;
  3. 复杂场景下要兼顾可读性性能:拆分长链式操作、谨慎使用并行流、做好空值处理。

如果需要针对某个具体业务场景(比如多表关联、大数据量分页、自定义聚合函数)深入讲解,后续再出。