java.time.LocalDate 是 Java 8 引入的 现代日期时间 API 的核心类之一,用于表示一个 不包含时间的日期,格式为 yyyy-MM-dd(例如:2025-08-15)。它属于 java.time 包,是处理日期逻辑的推荐方式,替代了旧的 java.util.DateCalendar


一、核心概念

  1. 只表示日期,不含时间与时区

    • LocalDate 表示一个 年-月-日 的三元组,如 2025-08-15
    • 不包含 时、分、秒、毫秒,也 不包含 时区信息。
    • 适用于生日、节假日、合同生效日等“纯日期”场景。
  2. 不可变性(Immutable)

    • LocalDate 对象是 不可变的。所有修改操作(如加减天数)都会返回一个新的 LocalDate 实例,原对象不变。
    • ✅ 线程安全,适合多线程环境。
  3. 基于 ISO-8601 标准

    • 遵循国际标准 ISO-8601,日期格式为 yyyy-MM-dd
  4. 不可变性带来的好处

    • 避免意外修改。
    • 天然线程安全。
    • 易于缓存和共享。

二、操作步骤(非常详细)

步骤 1:创建 LocalDate 对象

1.1 获取当前日期(系统默认时区)

import java.time.LocalDate;

// 获取当前日期
LocalDate today = LocalDate.now();
System.out.println("今天: " + today); // 输出:2025-08-15

1.2 指定时区获取当前日期

import java.time.ZoneId;

// 获取特定时区的当前日期
LocalDate todayInShanghai = LocalDate.now(ZoneId.of("Asia/Shanghai"));
LocalDate todayInLondon = LocalDate.now(ZoneId.of("Europe/London"));

1.3 指定年、月、日创建

// 方法一:使用 of(int year, int month, int day)
LocalDate specificDate = LocalDate.of(2025, 8, 15); // 2025年8月15日

// 方法二:使用 Month 枚举(更清晰)
import java.time.Month;
LocalDate birthday = LocalDate.of(1990, Month.JANUARY, 1);

// 方法三:从字符串解析
String dateString = "2025-08-15";
LocalDate parsedDate = LocalDate.parse(dateString); // 默认格式 yyyy-MM-dd

1.4 从其他类型转换

import java.util.Date;
import java.time.Instant;

// 从 Date 转换(需通过 Instant)
Date date = new Date();
LocalDate fromDate = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();

// 从 Instant 转换
Instant instant = Instant.now();
LocalDate fromInstant = instant.atZone(ZoneId.systemDefault()).toLocalDate();

步骤 2:获取日期字段

LocalDate date = LocalDate.of(2025, 8, 15);

int year = date.getYear();           // 2025
int monthValue = date.getMonthValue(); // 8 (1-12)
Month month = date.getMonth();       // AUGUST (枚举)
int dayOfMonth = date.getDayOfMonth(); // 15
DayOfWeek dayOfWeek = date.getDayOfWeek(); // FRIDAY (枚举)
int dayOfYear = date.getDayOfYear();   // 227 (一年中的第几天)

步骤 3:日期计算(加减操作)

⚠️ 所有操作返回 新对象,原对象不变。

LocalDate date = LocalDate.of(2025, 8, 15);

// 加减年、月、日
LocalDate nextYear = date.plusYears(1);     // 2026-08-15
LocalDate lastMonth = date.minusMonths(1);  // 2025-07-15
LocalDate tomorrow = date.plusDays(1);      // 2025-08-16

// 使用 Period(更灵活)
import java.time.Period;
LocalDate future = date.plus(Period.ofYears(1).ofMonths(2).ofDays(5));
// 等价于 plusYears(1).plusMonths(2).plusDays(5)

步骤 4:日期比较

LocalDate date1 = LocalDate.of(2025, 8, 15);
LocalDate date2 = LocalDate.of(2025, 9, 1);

// 使用 compareTo()
int result = date1.compareTo(date2);
if (result < 0) System.out.println("date1 在 date2 之前");

// 使用 isBefore(), isAfter(), isEqual()
if (date1.isBefore(date2)) {
    System.out.println("date1 在 date2 之前");
}

if (date2.isAfter(date1)) {
    System.out.println("date2 在 date1 之后");
}

LocalDate date3 = LocalDate.of(2025, 8, 15);
if (date1.isEqual(date3)) {
    System.out.println("两个日期相等");
}

步骤 5:格式化与解析

5.1 格式化为字符串

import java.time.format.DateTimeFormatter;

LocalDate date = LocalDate.now();

// 使用预定义格式
String iso = date.format(DateTimeFormatter.ISO_LOCAL_DATE); // yyyy-MM-dd
String isoCustom = date.format(DateTimeFormatter.ofPattern("yyyy年MM月dd日")); // 2025年08月15日

// 自定义格式
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("d MMMM yyyy", java.util.Locale.CHINA);
String formatted = date.format(formatter); // 15 八月 2025

5.2 从字符串解析

String input = "2025/08/15";
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");

try {
    LocalDate parsed = LocalDate.parse(input, formatter);
    System.out.println("解析成功: " + parsed);
} catch (java.time.format.DateTimeParseException e) {
    System.err.println("解析失败: 格式不匹配");
}

步骤 6:其他实用操作

LocalDate date = LocalDate.of(2025, 2, 15);

// 判断是否为闰年
boolean isLeap = date.isLeapYear(); // true (2025 不是闰年?错!2024 是,2025 否)

// 获取当月的第一天/最后一天
LocalDate firstDay = date.withDayOfMonth(1);           // 2025-02-01
LocalDate lastDay = date.withDayOfMonth(date.lengthOfMonth()); // 2025-02-28

// 或使用 TemporalAdjusters(更优雅)
import java.time.temporal.TemporalAdjusters;
LocalDate firstDayOfMonth = date.with(TemporalAdjusters.firstDayOfMonth());
LocalDate lastDayOfMonth = date.with(TemporalAdjusters.lastDayOfMonth());

// 下一个周日
LocalDate nextSunday = date.with(TemporalAdjusters.next(DayOfWeek.SUNDAY));

三、常见错误

  1. 误以为 LocalDate 包含时间或时区

    • LocalDate 只有日期。需要时间用 LocalDateTime,需要时区用 ZonedDateTime
  2. 忘记处理异常

    • LocalDate.parse() 可能抛出 DateTimeParseException,需 try-catch
  3. 修改原对象

    • date.plusDays(1); 不会修改 date,必须接收返回值:
      date = date.plusDays(1); // 正确
      
  4. 格式不匹配

    • 解析时 DateTimeFormatter 的模式必须与字符串完全一致。

四、注意事项

  1. LocalDate 是不可变的:所有操作都返回新实例。
  2. 默认格式为 ISO-8601toString() 输出 yyyy-MM-dd
  3. 月份从 1 开始getMonthValue() 返回 1-12,避免了 Calendar 的 0-11 陷阱。
  4. 时区在创建时确定LocalDate.now() 使用 JVM 默认时区,跨时区部署时需注意。

五、使用技巧

  1. 使用 TemporalAdjusters 简化复杂计算

    LocalDate nextWorkday = date.with(TemporalAdjusters.nextOrSame(DayOfWeek.MONDAY));
    
  2. 缓存常用格式化器

    private static final DateTimeFormatter FORMATTER = 
        DateTimeFormatter.ofPattern("yyyy年MM月dd日");
    
  3. 结合 Period 进行灵活计算

    Period period = Period.between(startDate, endDate); // 计算两个日期间的年月日差
    

六、最佳实践与性能优化

最佳实践

  1. 优先使用 LocalDate 而非 Date

    • 对于纯日期逻辑,LocalDate 更清晰、更安全。
  2. 明确时区

    • 使用 LocalDate.now(ZoneId) 而非无参 now(),避免依赖默认时区。
  3. 使用常量格式化器

    • DateTimeFormatter 是线程安全的,可声明为 static final
  4. 避免 null

    • 使用 Optional<LocalDate> 或默认值处理可能为空的日期。

性能优化

  1. 重用 DateTimeFormatter

    • 创建成本较高,应缓存复用。
  2. 避免频繁创建 LocalDate

    • 如“今天”可缓存(注意过期)。
  3. 使用 long 时间戳进行大规模计算

    • 对于大量日期计算,可转换为 toEpochDay()(从 1970-01-01 起的天数)进行 long 运算。

总结对比

特性 java.util.Date java.time.LocalDate
是否包含时间
是否包含时区 否(但显示时依赖)
是否可变 否 ✅
线程安全 是 ✅
API 清晰度 差(过时方法多) 好 ✅
推荐程度 ❌ 旧代码维护 ✅ 新项目首选