java.time.LocalDate
是 Java 8 引入的 现代日期时间 API 的核心类之一,用于表示一个 不包含时间的日期,格式为 yyyy-MM-dd
(例如:2025-08-15)。它属于 java.time
包,是处理日期逻辑的推荐方式,替代了旧的 java.util.Date
和 Calendar
。
一、核心概念
只表示日期,不含时间与时区
LocalDate
表示一个 年-月-日 的三元组,如2025-08-15
。- 它 不包含 时、分、秒、毫秒,也 不包含 时区信息。
- 适用于生日、节假日、合同生效日等“纯日期”场景。
不可变性(Immutable)
LocalDate
对象是 不可变的。所有修改操作(如加减天数)都会返回一个新的LocalDate
实例,原对象不变。- ✅ 线程安全,适合多线程环境。
基于 ISO-8601 标准
- 遵循国际标准 ISO-8601,日期格式为
yyyy-MM-dd
。
- 遵循国际标准 ISO-8601,日期格式为
不可变性带来的好处
- 避免意外修改。
- 天然线程安全。
- 易于缓存和共享。
二、操作步骤(非常详细)
步骤 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));
三、常见错误
误以为
LocalDate
包含时间或时区:- ❌
LocalDate
只有日期。需要时间用LocalDateTime
,需要时区用ZonedDateTime
。
- ❌
忘记处理异常:
LocalDate.parse()
可能抛出DateTimeParseException
,需try-catch
。
修改原对象:
- ❌
date.plusDays(1);
不会修改date
,必须接收返回值:date = date.plusDays(1); // 正确
- ❌
格式不匹配:
- 解析时
DateTimeFormatter
的模式必须与字符串完全一致。
- 解析时
四、注意事项
LocalDate
是不可变的:所有操作都返回新实例。- 默认格式为 ISO-8601:
toString()
输出yyyy-MM-dd
。 - 月份从 1 开始:
getMonthValue()
返回 1-12,避免了Calendar
的 0-11 陷阱。 - 时区在创建时确定:
LocalDate.now()
使用 JVM 默认时区,跨时区部署时需注意。
五、使用技巧
使用
TemporalAdjusters
简化复杂计算:LocalDate nextWorkday = date.with(TemporalAdjusters.nextOrSame(DayOfWeek.MONDAY));
缓存常用格式化器:
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy年MM月dd日");
结合
Period
进行灵活计算:Period period = Period.between(startDate, endDate); // 计算两个日期间的年月日差
六、最佳实践与性能优化
最佳实践
优先使用
LocalDate
而非Date
:- 对于纯日期逻辑,
LocalDate
更清晰、更安全。
- 对于纯日期逻辑,
明确时区:
- 使用
LocalDate.now(ZoneId)
而非无参now()
,避免依赖默认时区。
- 使用
使用常量格式化器:
DateTimeFormatter
是线程安全的,可声明为static final
。
避免 null:
- 使用
Optional<LocalDate>
或默认值处理可能为空的日期。
- 使用
性能优化
重用
DateTimeFormatter
:- 创建成本较高,应缓存复用。
避免频繁创建
LocalDate
:- 如“今天”可缓存(注意过期)。
使用
long
时间戳进行大规模计算:- 对于大量日期计算,可转换为
toEpochDay()
(从 1970-01-01 起的天数)进行long
运算。
- 对于大量日期计算,可转换为
总结对比
特性 | java.util.Date |
java.time.LocalDate |
---|---|---|
是否包含时间 | 是 | 否 |
是否包含时区 | 否(但显示时依赖) | 否 |
是否可变 | 是 | 否 ✅ |
线程安全 | 否 | 是 ✅ |
API 清晰度 | 差(过时方法多) | 好 ✅ |
推荐程度 | ❌ 旧代码维护 | ✅ 新项目首选 |