Loading...

Java Lambda 表达式与函数式接口完全指南:从 CRUD 业务到 JDK 11/17 实战

一、为什么 Lambda 在 CRUD 业务中如此重要

在 Java 8 之前,处理集合数据、构建数据库查询条件、编写回调逻辑,几乎都离不开匿名内部类和冗长的 for 循环。以最简单的“从用户列表中筛选出指定状态的用户”为例:

// Java 7 写法
List<User> activeUsers = new ArrayList<>();
for (User user : userList) {
    if (user.getStatus() == 1) {
        activeUsers.add(user);
    }
}

Java 8 引入 Lambda 表达式和 Stream API 后,同一段逻辑可以压缩为:

List<User> activeUsers = userList.stream()
        .filter(u -> u.getStatus() == 1)
        .collect(Collectors.toList());

代码量减少了约 70%,意图更加清晰。而真正让 Lambda 在 CRUD 业务中“不可或缺”的,是它与函数式接口的组合——这使得行为参数化成为可能,增删改查的每一个环节都可以用声明式的风格来表达。

Lambda 表达式的本质:它是一个匿名函数,用来实现函数式接口的唯一抽象方法。语法形式为 (参数) -> { 方法体 },根据方法体的复杂度可以简写为 (参数) -> 表达式。

函数式接口:只有一个抽象方法的接口(可以用 @FunctionalInterface 标注)。Lambda 表达式必须有“插槽”来接住它,这个插槽就是函数式接口。

下面用一个统一的 User 实体贯穿全文,所有示例都围绕它展开:

public class User {
    private Long id;
    private String username;
    private Integer age;
    private Integer status;      // 1=正常, 2=冻结
    private BigDecimal balance;

    // 构造器、getter/setter 省略
}

二、四大核心函数式接口:CRUD 的原子积木

Java 8 在 java.util.function 包中提供了大量函数式接口,其中Function、Predicate、Consumer、Supplier 是使用频率最高的四个。理解它们的“输入输出契约”,是写出干净 Lambda 代码的前提。

2.1 Function<T, R>——转换型接口

签名:R apply(T t)——接收一个 T 类型参数,返回一个 R 类型结果。

CRUD 场景:实体到 DTO 的转换、字段提取。

// 场景:将 User 对象转换为“用户名 + 年龄”的展示字符串
Function<User, String> toDisplay = user -> 
        user.getUsername() + " (" + user.getAge() + "岁)";

// 用于列表转换
List<User> users = Arrays.asList(
        new User(1L, "张三", 25, 1, new BigDecimal("5000")),
        new User(2L, "李四", 30, 2, new BigDecimal("8000"))
);
List<String> displays = users.stream()
        .map(toDisplay)          // 直接使用方法引用更简洁:User::toDisplayString
        .collect(Collectors.toList());
// 结果:["张三 (25岁)", "李四 (30岁)"]

// Function 的链式组合:先提取用户名,再转大写
Function<User, String> usernameUpper = ((Function<User, String>) User::getUsername)
        .andThen(String::toUpperCase);
String result = usernameUpper.apply(users.get(0)); // "张三".toUpperCase()

关键点:Function 的默认方法 andThen 和 compose 允许将多个转换函数串联成流水线。在 CRUD 中,这常用于“查询 → 转换 → 输出”的链路。

2.2 Predicate<T>——断定型接口

签名:boolean test(T t)——接收一个 T 类型参数,返回布尔值。

CRUD 场景:查询过滤条件、删除前的安全检查、更新前的校验。

// 场景:组合多个查询条件
Predicate<User> isActive = user -> user.getStatus() == 1;
Predicate<User> isAdult = user -> user.getAge() >= 18;
Predicate<User> hasBalance = user -> user.getBalance().compareTo(BigDecimal.ZERO) > 0;

// 组合:状态正常 且 成年 且 余额大于0
Predicate<User> canTransact = isActive.and(isAdult).and(hasBalance);

// 在查询中使用
List<User> eligible = users.stream()
        .filter(canTransact)
        .collect(Collectors.toList());

// 在删除前使用:只有冻结状态的用户才允许删除
Predicate<User> canDelete = user -> user.getStatus() == 2;

// 在更新前使用:只有余额充足才执行扣减
Predicate<User> hasSufficientBalance = user -> 
        user.getBalance().compareTo(new BigDecimal("100")) >= 0;

默认方法:and、or、negate 三个方法使得条件组合变得极其灵活,这在构建动态查询条件时非常实用。

2.3 Consumer<T>——消费型接口

签名:void accept(T t)——接收一个 T 类型参数,无返回值。

CRUD 场景:遍历打印、日志记录、更新前的字段填充、批量操作中的副作用。

// 场景:批量更新用户状态,同时记录日志
Consumer<User> freezeUser = user -> {
    user.setStatus(2);
    log.info("用户 [{}] 已被冻结", user.getUsername());
};

// 批量处理
users.stream()
        .filter(user -> user.getBalance().compareTo(new BigDecimal("1000")) < 0)
        .forEach(freezeUser);

// Consumer 的链式组合:先记录日志,再更新字段
Consumer<User> auditAndUpdate = ((Consumer<User>) user -> 
        log.info("准备更新用户: {}", user.getUsername()))
        .andThen(user -> user.setStatus(1));

注意:Consumer 的方法体通常包含副作用(side effect),在 Stream 操作中应谨慎使用 forEach,优先考虑用 map 生成新集合来保持函数式的纯度。

2.4 Supplier<T>——供给型接口

签名:T get()——无参数,返回一个 T 类型结果。

CRUD 场景:对象工厂、默认值提供、延迟初始化。

// 场景:创建默认的“访客用户”模板
Supplier<User> guestSupplier = () -> {
    User guest = new User();
    guest.setUsername("guest");
    guest.setStatus(1);
    guest.setBalance(BigDecimal.ZERO);
    return guest;
};

// 使用:每次调用生成一个新的实例
User newGuest = guestSupplier.get();

// 在 CRUD 中的典型用法:为查询结果提供默认值
Supplier<List<User>> emptyListSupplier = ArrayList::new;
List<User> results = Optional.ofNullable(rawResults)
        .orElseGet(emptyListSupplier);

2.5 四大接口对照速查

接口输入输出核心方法CRUD 典型用途
Function<T,R>TRapply实体→DTO 转换、字段提取
Predicate<T>Tbooleantest查询条件、删除/更新前校验
Consumer<T>Tvoidaccept遍历处理、日志记录、批量更新
Supplier<T>无Tget对象工厂、默认值、延迟初始化

此外,BiFunction<T,U,R>(两个输入一个输出)、BiConsumer<T,U>(两个输入无输出)、UnaryOperator<T>(T→T,如 str -> str.toUpperCase())、BinaryOperator<T>(T,T→T,如 (a,b) -> a+b)也是 CRUD 中常用的变体。

三、Collection 新增的 Lambda 友好方法:CRUD 的直接支持

Java 8 为 Collection 体系新增了一批接受函数式接口的方法,这些方法是 CRUD 操作最直接的 Lambda 入口。

3.1 查询(Read)

Collection.forEach(Consumer):遍历集合。

users.forEach(user -> 
        System.out.println(user.getId() + ": " + user.getUsername()));

Collection.stream():获取流,配合 filter/map/sorted 实现任意复杂查询。

// 组合查询:状态正常且余额大于5000,按余额降序,取前3个用户名
List<String> topNames = users.stream()
        .filter(u -> u.getStatus() == 1)
        .filter(u -> u.getBalance().compareTo(new BigDecimal("5000")) > 0)
        .sorted(Comparator.comparing(User::getBalance).reversed())
        .limit(3)
        .map(User::getUsername)
        .collect(Collectors.toList());

Stream.filter(Predicate):条件过滤,是“查”的核心操作。Stream.map(Function):字段提取或类型转换。Stream.sorted(Comparator):排序,Comparator.comparing 配合 Lambda 非常简洁。

3.2 增加(Create)

// 使用 Supplier 批量生成
Supplier<User> defaultUser = () -> {
    User u = new User();
    u.setStatus(1);
    u.setBalance(BigDecimal.ZERO);
    return u;
};

List<User> newUsers = IntStream.range(0, 10)
        .mapToObj(i -> {
            User u = defaultUser.get();  // 每次获取新实例
            u.setUsername("user_" + i);
            return u;
        })
        .collect(Collectors.toList());

3.3 修改(Update)

Collection.replaceAll(UnaryOperator<E>):原地替换所有元素。

// 将所有用户的余额统一增加 1000
users.replaceAll(user -> {
    user.setBalance(user.getBalance().add(new BigDecimal("1000")));
    return user;
});

List.sort(Comparator):原地排序,Comparator 用 Lambda 表达。

users.sort(Comparator.comparing(User::getAge).thenComparing(User::getUsername));

Map.replaceAll(BiFunction<K,V,V>):原地替换 Map 中的所有值

Map<Long, User> userMap = new HashMap<>();
userMap.replaceAll((id, user) -> {
    user.setStatus(1);  // 所有用户恢复为正常状态
    return user;
});

3.4 删除(Delete)

Collection.removeIf(Predicate):按条件删除元素,是“删”的最佳 Lambda 写法。

// 删除所有冻结状态的用户
users.removeIf(user -> user.getStatus() == 2);

// 组合条件删除:余额为0且状态为冻结
users.removeIf(user -> user.getStatus() == 2 
        && user.getBalance().compareTo(BigDecimal.ZERO) == 0);

四、方法引用:Lambda 的极致简写

方法引用是 Lambda 表达式的语法糖,当 Lambda 体只是“调用一个已有方法”时,可以用 :: 进一步简化。

类型Lambda 写法方法引用CRUD 场景
静态方法s -> Integer.parseInt(s)Integer::parseInt类型转换
实例方法(特定对象)s -> System.out.println(s)System.out::println打印/日志
实例方法(任意对象)u -> u.getUsername()User::getUsername字段提取
构造器() -> new User()User::new对象工厂
// 用方法引用替代 Lambda
users.stream().map(User::getUsername).collect(Collectors.toList());
users.forEach(System.out::println);
users.removeIf(User::isFrozen);  // 假设 isFrozen() 返回 boolean

// 构造器引用 + Supplier
Supplier<User> userFactory = User::new;

选择原则:如果 Lambda 体只是调用一个已有方法,优先用方法引用,可读性更好。

五、数据库 CRUD 中的 Lambda:MyBatis-Plus 的 LambdaQueryWrapper

在实际企业开发中,Lambda 在 CRUD 中最大的“战场”是条件构造器。MyBatis-Plus 提供了 LambdaQueryWrapper 和 LambdaUpdateWrapper,用方法引用代替字符串字段名,实现了类型安全的动态 SQL 构建。

5.1 为什么要用 LambdaQueryWrapper

传统 QueryWrapper 需要硬编码数据库字段名:

QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("username", "张三");  // "username" 是字符串,拼错不会报错

LambdaQueryWrapper 用方法引用:

LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUsername, "张三");  // 字段名错误在编译期就会暴露

核心优势:编译期类型安全、重构友好(字段改名时 IDE 可自动更新)、无需记忆数据库字段名。

5.2 查询(Read)

// 服务层 Lambda 查询(IService 风格)
public List<User> queryUserList(String name, Integer status, 
                                 BigDecimal minBalance, BigDecimal maxBalance) {
    return lambdaQuery()
            .like(name != null, User::getUsername, name)           // 条件为 null 时自动跳过
            .eq(status != null, User::getStatus, status)
            .between(minBalance != null && maxBalance != null, 
                     User::getBalance, minBalance, maxBalance)
            .orderByDesc(User::getBalance)
            .list();
}

关键机制:每个条件方法都接受一个 boolean condition 作为第一个参数。当条件为 false 时,该条件不会拼接到 SQL 中,实现了动态 SQL 的优雅表达。

等值查询与模糊查询:

wrapper.eq(User::getStatus, 1);                    // status = 1
wrapper.like(User::getUsername, "张");              // username LIKE '%张%'
wrapper.likeRight(User::getUsername, "张");         // username LIKE '张%'
wrapper.in(User::getId, Arrays.asList(1L, 2L, 3L)); // id IN (1,2,3)
wrapper.isNull(User::getBalance);                   // balance IS NULL

5.3 更新(Update)

// 扣减余额,扣减后为0则冻结
public void updateUserBalance(Long id, String username, BigDecimal amount) {
    lambdaUpdate()
            .setSql("balance = balance - " + amount)      // 原生 SQL 表达式
            .set(amount.compareTo(BigDecimal.ZERO) == 0, 
                 User::getStatus, 2)                       // 条件为 true 时才 SET
            .eq(id != null, User::getId, id)
            .eq(username != null, User::getUsername, username)
            .update();
}

setSql 允许直接写入 SQL 片段(如 balance - amount),适合算术运算;set 的 condition 参数实现了“条件更新”。

5.4 删除(Delete)

// 按条件删除
lambdaUpdate()
        .eq(User::getStatus, 2)
        .remove();  // 逻辑删除或物理删除取决于配置

// 使用 Mapper 的 Lambda 删除
LambdaQueryWrapper<User> deleteWrapper = new LambdaQueryWrapper<>();
deleteWrapper.eq(User::getStatus, 2)
             .lt(User::getBalance, new BigDecimal("100"));
userMapper.delete(deleteWrapper);

5.5 新增(Create)

新增本身不涉及条件构造,但 Lambda 在批量新增中用于数据准备:

// 批量新增的 Lambda 数据生成
List<User> batch = IntStream.range(0, 10000)
        .mapToObj(i -> {
            User u = new User();
            u.setUsername("batch_user_" + i);
            u.setStatus(1);
            u.setBalance(new BigDecimal("1000"));
            return u;
        })
        .collect(Collectors.toList());
saveBatch(batch);

六、Stream 链式操作在 CRUD 中的完整实战

Stream API 将查询、过滤、转换、收集串联成一条声明式管道。理解“中间操作”与“终端操作”的分界是关键:filter、map、sorted、limit、distinct 是中间操作(惰性),collect、forEach、count、findFirst、reduce 是终端操作(触发执行)。

6.1 查询:多条件筛选 + 排序 + 分页

public List<User> searchUsers(List<User> all, UserQuery query) {
    return all.stream()
            .filter(u -> query.getName() == null 
                    || u.getUsername().contains(query.getName()))
            .filter(u -> query.getStatus() == null 
                    || u.getStatus().equals(query.getStatus()))
            .filter(u -> query.getMinAge() == null 
                    || u.getAge() >= query.getMinAge())
            .sorted(Comparator.comparing(User::getAge)
                    .thenComparing(User::getUsername))
            .skip((long) query.getPage() * query.getSize())  // 分页:跳过
            .limit(query.getSize())                          // 分页:取多少
            .collect(Collectors.toList());
}

6.2 聚合与统计

// 按状态分组统计
Map<Integer, List<User>> byStatus = users.stream()
        .collect(Collectors.groupingBy(User::getStatus));

// 按状态分组,再统计每组余额之和
Map<Integer, BigDecimal> balanceByStatus = users.stream()
        .collect(Collectors.groupingBy(
                User::getStatus,
                Collectors.reducing(
                        BigDecimal.ZERO,
                        User::getBalance,
                        BigDecimal::add
                )
        ));

// 取年龄最大的用户
Optional<User> oldest = users.stream()
        .max(Comparator.comparing(User::getAge));

// 是否存在冻结用户
boolean hasFrozen = users.stream()
        .anyMatch(u -> u.getStatus() == 2);

6.3 去重与集合转换

// 提取用户名去重
List<String> distinctNames = users.stream()
        .map(User::getUsername)
        .distinct()
        .collect(Collectors.toList());

// 转换为 Map:id -> User
Map<Long, User> idMap = users.stream()
        .collect(Collectors.toMap(User::getId, Function.identity()));

// 转换为 Map 并处理 key 冲突
Map<String, User> nameMap = users.stream()
        .collect(Collectors.toMap(
                User::getUsername,
                Function.identity(),
                (existing, replacement) -> existing  // 保留先出现的
        ));

七、JDK 11 对 Lambda 的增强

7.1 Lambda 参数使用 var(JEP 323)

Java 11 允许在 Lambda 参数中使用 var,这本身不会改变类型推断的行为,但使得在 Lambda 参数上添加注解成为可能——以前必须在参数前写出完整类型名,现在可以用 var 占位。

// Java 11 之前:Lambda 参数无法添加注解(必须写出完整类型)
// Java 11:使用 var 后可以添加注解
list.stream()
    .map((@NotNull var user) -> user.getUsername().toUpperCase())
    .collect(Collectors.toList());

这个特性在需要标注 @Nullable、@NotNull 等校验注解时非常有用,尤其在配合框架的校验机制时。

7.2 其他与 Lambda 间接相关的实用增强

String 新增方法:isBlank()、lines()、strip()、repeat(n) 等,在 CRUD 中处理文本字段时非常实用。

// isBlank:判断空白字符串,比 trim().isEmpty() 更准确
list.stream()
    .filter(u -> u.getUsername() != null && !u.getUsername().isBlank())
    .collect(Collectors.toList());

// lines:将多行字符串转为 Stream
String multiLine = "user1\nuser2\nuser3";
List<String> names = multiLine.lines()
        .filter(s -> !s.isBlank())
        .collect(Collectors.toList());

HTTP Client 标准化(JEP 321):Java 11 将 HTTP Client API 转为正式特性,配合 CompletableFuture 的异步回调天然适合 Lambda。

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create("https://api.example.com/users"))
        .GET()
        .build();

client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
        .thenApply(HttpResponse::body)
        .thenAccept(body -> log.info("响应: {}", body));  // Lambda 作为回调

八、JDK 17 对 Lambda 的增强与协同特性

JDK 17 作为 LTS 版本,虽然没有直接修改 Lambda 语法,但引入了几个与 Lambda 配合极佳的特性,能够显著提升 CRUD 代码的表达力。

8.1 Record 类(JEP 395)——数据载体的极简化

Record 是“透明数据载体”的紧凑写法,替代了传统 CRUD 中大量编写的 DTO/VO/实体类样板代码。

// 传统 DTO:需要构造器、getter、equals、hashCode、toString
// Record 写法:一行完成
public record UserDTO(Long id, String username, Integer age, String statusDesc) {}

// CRUD 中的使用:从实体转换为 DTO
List<UserDTO> dtos = users.stream()
        .map(u -> new UserDTO(
                u.getId(),
                u.getUsername(),
                u.getAge(),
                u.getStatus() == 1 ? "正常" : "冻结"
        ))
        .collect(Collectors.toList());

// Record 作为查询参数容器
public record UserQuery(String name, Integer status, BigDecimal minBalance) {}

// 使用
public List<User> search(UserQuery query) {
    return users.stream()
            .filter(u -> query.name() == null 
                    || u.getUsername().contains(query.name()))
            .filter(u -> query.status() == null 
                    || u.getStatus().equals(query.status()))
            .collect(Collectors.toList());
}

Record 的关键约束:字段不可变(final),自动生成 equals/hashCode/toString,不可继承。这使它天然适合作为 CRUD 中的查询参数、API 响应、值对象。

8.2 密封类(JEP 409)——限定继承的精准控制

密封类允许一个类或接口精确指定哪些类可以继承/实现它,配合 switch 模式匹配可以实现穷尽性的类型分发。

// 定义 CRUD 操作结果的密封接口
public sealed interface OperationResult permits Success, Failure, NotFound {}

public record Success<T>(T data) implements OperationResult {}
public record Failure(String message, int code) implements OperationResult {}
public record NotFound(Long id) implements OperationResult {}

// 使用 switch 模式匹配分发(JDK 17 预览特性,需启用 --enable-preview)
public String handleResult(OperationResult result) {
    return switch (result) {
        case Success<?> s -> "操作成功: " + s.data();
        case Failure f -> "操作失败[" + f.code() + "]: " + f.message();
        case NotFound n -> "资源不存在: id=" + n.id();
    };
}

密封类 + Record + switch 模式匹配的组合,在 CRUD 中用于统一结果处理非常优雅,编译期即可确保所有情况被覆盖。

8.3 Switch 表达式(JEP 361)——更简洁的条件分发

Switch 表达式用 -> 替代 :,消除了贯穿语义,并且可以返回值。

// 根据用户状态返回描述
public String getStatusDesc(Integer status) {
    return switch (status) {
        case 1 -> "正常";
        case 2 -> "冻结";
        case 3 -> "注销";
        default -> "未知";
    };
}

// 在 Stream 中使用 switch 表达式
List<String> statuses = users.stream()
        .map(u -> switch (u.getStatus()) {
            case 1 -> "正常";
            case 2 -> "冻结";
            default -> "其他";
        })
        .collect(Collectors.toList());

8.4 文本块(JEP 378)——多行字符串的自然写法

在 CRUD 中,文本块最适合编写内嵌的 SQL 或 JSON 模板。

// 传统写法:转义 + 字符串拼接
String sql = "SELECT id, username, age, status, balance\n" +
             "FROM user\n" +
             "WHERE status = 1\n" +
             "ORDER BY balance DESC";

// 文本块写法
String sqlBlock = """
        SELECT id, username, age, status, balance
        FROM user
        WHERE status = 1
        ORDER BY balance DESC
        """;

九、综合实战:一个完整的用户 CRUD 服务

以下示例综合运用了本文所有知识点,展示一个内存版用户服务的完整实现,并标注每段代码对应的 Lambda/函数式技术点。

public class UserService {

    private final List<User> userStore = new CopyOnWriteArrayList<>();
    private final AtomicLong idGenerator = new AtomicLong(1);

    // ==================== 增(Create) ====================

    /** 新增单个用户。Supplier 用于生成默认值。 */
    public User create(String username, Integer age) {
        Supplier<User> factory = () -> {
            User u = new User();
            u.setId(idGenerator.getAndIncrement());
            u.setUsername(username);
            u.setAge(age);
            u.setStatus(1);
            u.setBalance(BigDecimal.ZERO);
            return u;
        };
        User user = factory.get();
        userStore.add(user);
        return user;
    }

    /** 批量新增。Function 用于字段映射。 */
    public List<User> batchCreate(List<String> names) {
        List<User> created = names.stream()
                .map(name -> create(name, 18))  // Function<String, User>
                .collect(Collectors.toList());
        return created;
    }

    // ==================== 删(Delete) ====================

    /** 按 ID 删除。removeIf 配合 Predicate。 */
    public boolean deleteById(Long id) {
        return userStore.removeIf(u -> u.getId().equals(id));
    }

    /** 按条件批量删除。Predicate 组合。 */
    public int deleteByCondition(Predicate<User> condition) {
        int before = userStore.size();
        userStore.removeIf(condition);
        return before - userStore.size();
    }

    /** 删除所有冻结用户 */
    public int deleteAllFrozen() {
        return deleteByCondition(u -> u.getStatus() == 2);
    }

    // ==================== 改(Update) ====================

    /** 更新用户信息。BiConsumer 接收 id 和更新操作。 */
    public void update(Long id, Consumer<User> updater) {
        userStore.stream()
                .filter(u -> u.getId().equals(id))
                .findFirst()
                .ifPresent(updater);  // Consumer 执行副作用
    }

    /** 批量将所有用户余额增加指定值。UnaryOperator 原地替换。 */
    public void increaseAllBalance(BigDecimal amount) {
        userStore.replaceAll(user -> {
            user.setBalance(user.getBalance().add(amount));
            return user;
        });
    }

    // ==================== 查(Read) ====================

    /** 按 ID 查询。Optional + Predicate。 */
    public Optional<User> findById(Long id) {
        return userStore.stream()
                .filter(u -> u.getId().equals(id))
                .findFirst();
    }

    /** 条件查询 + 排序 + 分页。完整 Stream 管道。 */
    public List<User> query(UserQuery query) {
        return userStore.stream()
                .filter(u -> query.name() == null 
                        || u.getUsername().contains(query.name()))
                .filter(u -> query.status() == null 
                        || u.getStatus().equals(query.status()))
                .filter(u -> query.minBalance() == null 
                        || u.getBalance().compareTo(query.minBalance()) >= 0)
                .sorted(Comparator.comparing(User::getAge)
                        .thenComparing(User::getUsername))
                .skip((long) query.page() * query.size())
                .limit(query.size())
                .collect(Collectors.toList());
    }

    /** 统计每个状态的用户数量。groupingBy + counting。 */
    public Map<Integer, Long> countByStatus() {
        return userStore.stream()
                .collect(Collectors.groupingBy(
                        User::getStatus,
                        Collectors.counting()
                ));
    }

    /** 查找余额最高的 N 个用户。sorted + limit。 */
    public List<User> findTopNByBalance(int n) {
        return userStore.stream()
                .sorted(Comparator.comparing(User::getBalance).reversed())
                .limit(n)
                .collect(Collectors.toList());
    }

    /** 判断是否存在指定用户名的用户。anyMatch。 */
    public boolean existsByUsername(String username) {
        return userStore.stream()
                .anyMatch(u -> u.getUsername().equals(username));
    }
}

// 配套的 Record 查询参数(JDK 16+)
public record UserQuery(String name, Integer status, 
                        BigDecimal minBalance, int page, int size) {
    // 紧凑构造器,提供默认值
    public UserQuery {
        if (page < 0) page = 0;
        if (size <= 0) size = 10;
    }
}

十、常见陷阱与最佳实践

陷阱 1:在 Lambda 中修改外部局部变量

Lambda 只能捕获 effectively final 的局部变量,不能修改。

// 编译错误:counter 不是 effectively final
int counter = 0;
users.forEach(u -> counter++);  // ❌

// 正确做法:用数组或 AtomicInteger
AtomicInteger counter = new AtomicInteger(0);
users.forEach(u -> counter.incrementAndGet());  // ✅

陷阱 2:Stream 的惰性求值

中间操作不会被立即执行,只有终端操作触发时才执行整条管道。

// 没有终端操作,什么都不会发生
users.stream().filter(u -> {
    System.out.println("筛选: " + u.getUsername());  // 不会打印
    return u.getStatus() == 1;
});

// 加上终端操作才会执行
users.stream()
     .filter(u -> u.getStatus() == 1)
     .collect(Collectors.toList());  // 此时才会遍历

陷阱 3:在 forEach 中修改集合

// ConcurrentModificationException
users.forEach(u -> {
    if (u.getStatus() == 2) {
        users.remove(u);  // ❌ 在遍历中修改集合
    }
});

// 正确:用 removeIf
users.removeIf(u -> u.getStatus() == 2);  // ✅


最佳实践清单

  1. 优先使用方法引用:当 Lambda 体只是调用已有方法时,User::getUsername 比 u -> u.getUsername() 更清晰。
  2. 方法引用中避免有副作用的 forEach:如果目的是转换数据,用 map + collect;只有在真正需要副作用(如打印、日志)时才用 forEach。
  3. 动态条件用 condition 参数:MyBatis-Plus 的条件方法第一个参数是 boolean,这是构建动态 SQL 的惯用法。
  4. Record 优先用于只读数据:查询参数、API 响应、值对象用 Record,实体类(需要可变状态)仍用普通类。
  5. JDK 17 中优先使用 switch 表达式:替代传统的 switch 语句和冗长的 if-else 链。
  6. 注意 Stream 的可读性:当管道超过 5-6 个操作时,考虑拆分为多个步骤或提取为命名方法。


以上内容覆盖了 Lambda 表达式在 CRUD 业务中的完整知识体系:从四大函数式接口的契约与组合,到 Collection 和 Stream 的增删改查操作,再到 MyBatis-Plus 的 LambdaQueryWrapper 实战,以及 JDK 11(var 参数、String 增强、HTTP Client)和 JDK 17(Record、密封类、Switch 表达式、文本块)的专属增强。掌握这些内容后,你应当能够在实际项目中写出类型安全、表达清晰、易于维护的 CRUD 代码。


0

回到顶部