日期时间救急锦囊: LocalDate / LocalDateTime / DateTimeFormatter
20 行学会打印当前时间、解析字符串、格式化输出,告别老 Date API。
一句话定调
Date 已退休,干活用 java.time 包:LocalDate 只管日期,LocalDateTime 管日期+时间,DateTimeFormatter 管格式化。
三个类怎么选:一张图分清
把 java.time 想成三块不同的表:LocalDate 是台历(只有年月日)、LocalTime 是秒表(只有时分秒)、LocalDateTime 是手机状态栏(日期时间都有)。它们都”不带时区”——表示的是”墙上时钟看到的值”;跨时区系统(用户分布在不同国家、需要精确时间戳)才轮到 ZonedDateTime(带时区的墙钟)和 Instant(秒表上的绝对时刻)。
| 类型 | 内容 | 典型场景 |
|---|---|---|
LocalDate | 年-月-日 | 生日、订单日期、报表日 |
LocalTime | 时:分:秒 | 营业时间、闹钟 |
LocalDateTime | 日期+时间 | 日志时间戳、预约单 |
ZonedDateTime | 日期+时间+时区 | 跨时区会议、航班 |
Instant | UTC 时间戳 | 数据库存档、接口传输 |
极简代码(看懂这 20 行就够了)
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class TimeQuickstart {
public static void main(String[] args) {
// 1. 打印当前日期 / 日期时间
LocalDate today = LocalDate.now();
LocalDateTime now = LocalDateTime.now();
System.out.println(today); // 2026-08-03
System.out.println(now); // 2026-08-03T10:30:00.123
// 2. 字符串 -> 时间对象(解析)
LocalDate d = LocalDate.parse("2026-08-03"); // ISO 格式可直接解析
LocalDateTime dt = LocalDateTime.parse("2026-08-03T10:30:00");
// 3. 时间对象 -> 自定义格式(格式化)
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
System.out.println(now.format(fmt)); // 2026-08-03 10:30
// 4. 常用运算
System.out.println(today.plusDays(7)); // 一周后
System.out.println(dt.minusHours(2)); // 两小时前
System.out.println(java.time.temporal.ChronoUnit.DAYS.between(
LocalDate.of(2026, 1, 1), today)); // 相差天数
}
}需要带时区的场景(跨时区系统)才用 ZonedDateTime 与 Instant,日常业务 LocalDate / LocalDateTime 足够。
最大的思维转换:不可变对象
java.time 的所有类都是不可变的——plusDays 不会改自己,而是返回一个新对象。类比字符串:"abc".toUpperCase() 不写 s = ... 接住结果,s 永远不变。日期同理:
LocalDate today = LocalDate.of(2026, 8, 3);
// 错误写法:计算结果没接住,today 纹丝不动
today.plusDays(7);
System.out.println(today); // 2026-08-03(不是一周后!)
// 正确写法:重新赋值
today = today.plusDays(7);
System.out.println(today); // 2026-08-10这是 java.time 新手第一坑,编译器不会报任何错。
完整示例:比较、周期与生日提醒
import java.time.LocalDate;
import java.time.Period;
import java.time.MonthDay;
import java.time.temporal.ChronoUnit;
public class TimeComplete {
public static void main(String[] args) {
LocalDate launch = LocalDate.of(2025, 12, 31);
LocalDate now = LocalDate.of(2026, 8, 3);
// 比较:isBefore / isAfter / isEqual
System.out.println(now.isAfter(launch)); // true
// Period:按"年月日"人话表示两个日期的差
Period age = Period.between(launch, now);
System.out.println(age.getYears() + "年" + age.getMonths() + "个月");
// 预期输出:0年7个月
// ChronoUnit:按单一单位换算
System.out.println(ChronoUnit.DAYS.between(launch, now)); // 215
System.out.println(ChronoUnit.MONTHS.between(launch, now)); // 7
// MonthDay:只关心月日,做生日/周年提醒
MonthDay birthday = MonthDay.of(9, 1);
LocalDate nextBirthday = birthday.atYear(now.getYear());
if (nextBirthday.isBefore(now)) {
nextBirthday = birthday.atYear(now.getYear() + 1);
}
System.out.println("距离下次生日还有 "
+ ChronoUnit.DAYS.between(now, nextBirthday) + " 天");
// 预期输出:距离下次生日还有 29 天
}
}间隔计算二选一:差值要”几年几月几天”用 Period,要”总天数”用 ChronoUnit.DAYS.between;时间差的对应物是 Duration(精确到纳秒的时长,不掺日历概念)。
动手练习(先遮住参考实现)
预计 25 分钟。老一辈 Java 课用 Calendar 推算「3 月 1 日的前一天是 2 月几号」来判断闰年,要写四五步;下面三道题都请用 java.time 解,做完回头看看省掉了什么。
练习一(5 分钟):今天的完整名片
打印今天的日期,格式为 2026-10-05 是星期一,是一年中的第 278 天。只用三个 API:LocalDate.now()、getDayOfWeek()、getDayOfYear()。自测:输出里星期与天数对得上今天的真实日期。
练习二(10 分钟):生日倒计时(处理 2 月 29 日)
复用上文生日提醒思路,但要求处理闰日:输入 MonthDay.of(2, 29),若目标年份不是闰年(如 2027),atYear(2027) 给出的日期是 2 月 28 日还是 3 月 1 日?先查 API 文档 MonthDay.atYear 对平年的行为形成预测,再运行验证,最后用一句话写下结论。
练习三(10 分钟):一行解「去年今天」与「当月最后一天」
写两行:输出去年的今天;输出本月最后一天的日期。只允许用 minusYears 与 TemporalAdjusters.lastDayOfMonth()(在 java.time.temporal 包里)。自测:把系统时间概念带进来验证——2 月的结论每年都该重算,你的两行代码自动正确吗?
参考实现(先别看)
import java.time.LocalDate;
import java.time.MonthDay;
import java.time.temporal.TemporalAdjusters;
public class DateDrill {
public static void main(String[] args) {
// 练习一
LocalDate today = LocalDate.now();
System.out.println(today + " 是星期" + today.getDayOfWeek().getValue()
+ ",是一年中的第 " + today.getDayOfYear() + " 天");
// 严格按中文习惯映射「一~日」可用 switch,此处保留 DayOfWeek 序数
// 练习二:atYear 对平年把 2 月 29 日回落为 2 月 28 日(查证后再看答案)
LocalDate leapDay = MonthDay.of(2, 29).atYear(2027); // 2027 不是闰年
System.out.println(leapDay); // 预期输出:2027-02-28
// 练习三
System.out.println(today.minusYears(1)); // 去年的今天
System.out.println(today.with(TemporalAdjusters.lastDayOfMonth())); // 本月最后一天
}
}预测题答案(先答再看):练习二的答案是回落到 2 月 28 日——MonthDay.atYear(平年) 把 2 月 29 日调整为当年 2 月的最后一个有效日,不报错也不顺延到 3 月。这是 API 替你做的日历决策,需求评审时把它当成明确定义写进文档。练习三不需要每年重算:minusYears 与 lastDayOfMonth 都基于日历规则现算(2028 年 2 月算出 29 日、2027 年算出 28 日),这正是「不可变对象 + 工厂方法」设计把日历数学收进库里的意义。
如果报这个错,看这里
报错:DateTimeParseException: Text '2026/08/03' could not be parsed
原因:LocalDate.parse() 默认只认 ISO 格式 yyyy-MM-dd,斜杠格式解析失败。
对策:先 DateTimeFormatter.ofPattern("yyyy/MM/dd") 再 LocalDate.parse(str, fmt)。
报错:UnsupportedTemporalTypeException: Unsupported field: HourOfDay
原因:对只有日期的 LocalDate 使用带 HH:mm 的格式器。
对策:LocalDate 用 yyyy-MM-dd,要带时分就用 LocalDateTime 或 LocalDateTime.now()。
报错:DateTimeParseException 但格式看起来一模一样
常见原因:字符串里有不可见字符(全角空格、零宽空格)或月份用了中文(“八月”)。对策:str.trim() 后再解析;中文月份需要 DateTimeFormatter.ofPattern("yyyy年MM月dd日") 配合本地化写法,或直接换成数字格式。
现象:多线程下格式化结果错乱或偶发异常
老 SimpleDateFormat 是线程不安全的(这是老 API 退休的原因之一);DateTimeFormatter 不可变、线程安全,可以作为 static final 常量全局复用,无需每次 new。
常用格式模式速查(ofPattern 字母含义)
| 模式字母 | 含义 | 示例输出 |
|---|---|---|
yyyy / yy | 4 位 / 2 位年份 | 2026 / 26 |
MM / M | 两位 / 一位月份 | 08 / 8 |
dd / d | 两位 / 一位日期 | 03 / 3 |
HH / hh | 24 小时 / 12 小时制小时 | 14 / 02 |
mm / ss | 分 / 秒 | 30 / 05 |
SSS | 毫秒 | 123 |
E / EEEE | 星期缩写 / 全称 | 周一 / 星期一 |
记忆口诀:同字母越长越”补零”,连写越长越”全称”。yyyy-MM-dd 与 YYYY-MM-dd 一字之差、谬以千里——大写 Y 是”周年(week-based year)“,跨年周会得出 2026 年 12 月 28 日这样的诡异结果,月份日期一律小写。
记住
初学者记住三点:
- 日期解析失败先怀疑格式;
LocalDate没有时分字段;跨时区才上ZonedDateTime。plusDays/minusHours返回新对象,必须用变量接住结果。DateTimeFormatter线程安全,写成常量复用;SimpleDateFormat别再用。
进阶者还需注意:
- “几年几个月”用
Period,“总时长”用Duration/ChronoUnit,两者混用会算错。 LocalDateTime不含时区语义,跨时区存档用Instant(UTC 时刻)+ 目标时区展示。- 日期运算跨月/闰年由 API 兜底(如 1 月 31 日
plusMonths(1)得 2 月 28 日),手写日历数学才是坑源。