Zademy

Java Lambda 表达式:集合和日期处理必备指南

Java; Lambdas; Stream; Dates; Tutorial
words 字

Java 8 的 lambda 表达式用了就回不去了。尤其是集合遍历和日期处理这两个场景,代码量直接砍半。这篇整理的是日常开发里出现频率最高的用法。

什么是 Lambda 表达式

本质上就是一个匿名函数,可以当参数传给方法。它的价值在于把"行为"当数据来用,代码不再被一堆匿名内部类撑得臃肿。

基本语法

(parameters) -> { function body }

看个对比就明白了:

// 使用匿名类的传统方式
Runnable r1 = new Runnable() {
    public void run() {
        System.out.println("Hello World");
    }
};

// Lambda 方式(更简洁)
Runnable r2 = () -> System.out.println("Hello World");

使用 Lambda 遍历集合

遍历数组

String[] names = {"Ana", "Carlos", "Beatriz", "David"};

// 使用 forEach 和 lambda 遍历
Arrays.stream(names)
      .forEach(name -> System.out.println("Hello " + name));

// 或使用方法引用
Arrays.stream(names)
      .forEach(System.out::println);

遍历列表

列表大概是 lambda 用得最多的地方。基本遍历、带索引、过滤——全都能用 Stream 写得干净利落。

List<String> fruits = Arrays.asList("Apple", "Banana", "Orange", "Strawberry");

// 基本遍历
fruits.forEach(fruit -> System.out.println("I like " + fruit));

// 带位置编号的遍历
IntStream.range(0, fruits.size())
         .forEach(i -> System.out.println(i + ": " + fruits.get(i)));

// 过滤和遍历
fruits.stream()
      .filter(fruit -> fruit.length() > 6)
      .forEach(fruit -> System.out.println("Long fruit: " + fruit));

遍历映射

Map 的 forEach 接受一个 BiConsumer,键值对同时拿到,写起来比 entrySet() 迭代舒服多了。

Map<String, Integer> ages = new HashMap<>();
ages.put("Ana", 25);
ages.put("Carlos", 32);
ages.put("Beatriz", 28);

// 遍历条目集(键值对)
ages.forEach((name, age) ->
    System.out.println(name + " is " + age + " years old")
);

// 仅遍历键
ages.keySet().forEach(key ->
    System.out.println("Person: " + key)
);

// 仅遍历值
ages.values().forEach(value ->
    System.out.println("Age: " + value)
);

使用 Lambda 处理日期

Java 8 的 java.time API 和 lambda 配合得很好。LocalDate、LocalTime、LocalDateTime 配上 Stream,过滤、排序、分组都写得很自然。

处理 LocalDate

LocalDate 只有日期,没有时间。

// 创建日期
LocalDate today = LocalDate.now();
LocalDate christmas = LocalDate.of(2025, 12, 25);

// 使用流操作日期
List<LocalDate> importantDates = Arrays.asList(
    LocalDate.of(2025, 1, 1),
    LocalDate.of(2025, 5, 10),
    LocalDate.of(2025, 9, 16),
    LocalDate.of(2025, 12, 25)
);

// 过滤未来日期
importantDates.stream()
    .filter(date -> date.isAfter(today))
    .forEach(date -> System.out.println("Future date: " + date));

// 格式化日期
importantDates.forEach(date ->
    System.out.println(date.format(DateTimeFormatter.ofPattern("MMMM dd, yyyy")))
);

处理 LocalTime

LocalTime 只有时间,没有日期。

// 创建时间
LocalTime now = LocalTime.now();
LocalTime lunch = LocalTime.of(14, 30);

// 一天中时间的列表
List<LocalTime> times = Arrays.asList(
    LocalTime.of(8, 0),   // 工作开始
    LocalTime.of(12, 0),  // 午餐
    LocalTime.of(14, 30), // 午餐后返回
    LocalTime.of(18, 0)   // 工作结束
);

// 过滤上午时间
times.stream()
    .filter(time -> time.isBefore(lunch))
    .forEach(time -> System.out.println("Morning: " + time));

// 转换为一天中的分钟数
times.forEach(time ->
    System.out.println("Minutes of day: " + time.toSecondOfDay() / 60)
);

处理 LocalDateTime

LocalDateTime 是日期加时间。

// 创建带时间的日期
LocalDateTime nowFull = LocalDateTime.now();
LocalDateTime meeting = LocalDateTime.of(2025, 11, 15, 10, 30);

// 事件列表
List<LocalDateTime> events = Arrays.asList(
    LocalDateTime.of(2025, 11, 8, 9, 0),   // 今天,上午会议
    LocalDateTime.of(2025, 11, 15, 10, 30), // 下次会议
    LocalDateTime.of(2025, 12, 25, 0, 0)    // 圣诞节
);

// 过滤当月事件
events.stream()
    .filter(event -> event.getMonth() == nowFull.getMonth())
    .forEach(event -> System.out.println("Event this month: " + event));

// 按日期排序事件
events.stream()
    .sorted()
    .forEach(event -> System.out.println(
        "Event: " + event.format(DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm"))
    ));

实用组合示例

处理带日期的人列表

集合遍历加日期计算,日常开发里经常组合使用。这里用 Period.between 算年龄,再过滤成年人。

// Person 类
class Person {
    private String name;
    private LocalDate birthDate;

    // 构造函数、getter...
}

List<Person> people = Arrays.asList(
    new Person("Ana", LocalDate.of(1995, 6, 15)),
    new Person("Carlos", LocalDate.of(1990, 12, 3)),
    new Person("Beatriz", LocalDate.of(1998, 8, 20))
);

// 计算年龄并过滤成年人
LocalDate today = LocalDate.now();
people.stream()
    .filter(p -> Period.between(p.getBirthDate(), today).getYears() >= 18)
    .forEach(p -> {
        int age = Period.between(p.getBirthDate(), today).getYears();
        System.out.println(p.getName() + " is " + age + " years old");
    });

按月份分组日期

Collectors.groupingBy 是 Stream API 里最实用的收集器之一,配合方法引用一行就能按属性分组。

// 使用 lambda 按月份分组日期
List<LocalDate> dates = Arrays.asList(
    LocalDate.of(2025, 1, 15),
    LocalDate.of(2025, 1, 20),
    LocalDate.of(2025, 2, 10),
    LocalDate.of(2025, 2, 25),
    LocalDate.of(2025, 3, 5)
);

Map<Month, List<LocalDate>> datesByMonth = dates.stream()
    .collect(Collectors.groupingBy(LocalDate::getMonth));

datesByMonth.forEach((month, dateList) -> {
    System.out.println("Month: " + month);
    dateList.forEach(date ->
        System.out.println("  - " + date.format(DateTimeFormatter.ofPattern("MM/dd")))
    );
});

最佳实践

方法引用

当 lambda 体只是调用一个已有方法时,方法引用比 lambda 更简洁。System.out::println 比 name -> System.out.println(name) 短,而且更易读。

// 而不是:name -> System.out.println(name)
names.forEach(System.out::println);

// 而不是:date -> date.getMonth()
dates.stream().map(LocalDate::getMonth).collect(Collectors.toList());

// 而不是:(String s) -> s.length()
words.stream().mapToInt(String::length).sum();

避免副作用

Stream 操作应该是无状态的。在 forEach 里修改外部集合看起来能跑,但并发场景下会出问题,而且破坏了 Stream 的声明式语义。

// ✅ 好:纯 lambda
List<Integer> doubled = numbers.stream()
    .map(n -> n * 2)
    .collect(Collectors.toList());

// ❌ 避免:带副作用的 lambda
List<Integer> result = new ArrayList<>();
numbers.forEach(n -> {
    n = n * 2;
    result.add(n);  // 修改外部状态
});

异常处理

lambda 里的受检异常很烦。标准函数式接口(Consumer、Function 等)的签名不允许抛出受检异常,所以要么在 lambda 内部 try-catch,要么抽个辅助方法包装。

// ✅ 在内部使用 try-catch
dates.forEach(date -> {
    try {
        // 可能抛出异常的代码
        LocalDate.parse(date.toString());
    } catch (DateTimeParseException e) {
        System.err.println("Invalid date: " + date);
    }
});

// ✅ 创建辅助方法
private static void processDate(LocalDate date) throws DateTimeParseException {
    // 可能抛出异常的代码
}

dates.forEach(LambdaGuide::processDate);  // 包装异常

结论

lambda 不只是语法糖。它改变了写 Java 代码的思维方式——从命令式的"怎么做"变成声明式的"做什么"。集合遍历和日期处理是入门最快的两个场景,先把这两个吃透,再去碰 flatMap、reduce、collect 这些更复杂的操作。

参考 Oracle 官方的 Lambda 表达式和 Stream API 教程。