前置知识: Java

日期时间救急锦囊: LocalDate / LocalDateTime / DateTimeFormatter

5 min入门

20 行学会打印当前时间、解析字符串、格式化输出,告别老 Date API。

一句话定调

Date 已退休,干活用 java.time 包:LocalDate 只管日期,LocalDateTime 管日期+时间,DateTimeFormatter 管格式化。

三个类怎么选:一张图分清

把 java.time 想成三块不同的表:LocalDate 是台历(只有年月日)、LocalTime 是秒表(只有时分秒)、LocalDateTime 是手机状态栏(日期时间都有)。它们都”不带时区”——表示的是”墙上时钟看到的值”;跨时区系统(用户分布在不同国家、需要精确时间戳)才轮到 ZonedDateTime(带时区的墙钟)和 Instant(秒表上的绝对时刻)。

类型内容典型场景
LocalDate年-月-日生日、订单日期、报表日
LocalTime时:分:秒营业时间、闹钟
LocalDateTime日期+时间日志时间戳、预约单
ZonedDateTime日期+时间+时区跨时区会议、航班
InstantUTC 时间戳数据库存档、接口传输

极简代码(看懂这 20 行就够了)

java
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 永远不变。日期同理:

java
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 新手第一坑,编译器不会报任何错。

完整示例:比较、周期与生日提醒

java
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 月的结论每年都该重算,你的两行代码自动正确吗?

参考实现(先别看)

java
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 / yy4 位 / 2 位年份2026 / 26
MM / M两位 / 一位月份08 / 8
dd / d两位 / 一位日期03 / 3
HH / hh24 小时 / 12 小时制小时14 / 02
mm / ss分 / 秒30 / 05
SSS毫秒123
E / EEEE星期缩写 / 全称周一 / 星期一

记忆口诀:同字母越长越”补零”,连写越长越”全称”。yyyy-MM-dd 与 YYYY-MM-dd 一字之差、谬以千里——大写 Y 是”周年(week-based year)“,跨年周会得出 2026 年 12 月 28 日这样的诡异结果,月份日期一律小写。

记住

初学者记住三点:

  1. 日期解析失败先怀疑格式;LocalDate 没有时分字段;跨时区才上 ZonedDateTime。
  2. plusDays/minusHours 返回新对象,必须用变量接住结果。
  3. DateTimeFormatter 线程安全,写成常量复用;SimpleDateFormat 别再用。

进阶者还需注意:

  • “几年几个月”用 Period,“总时长”用 Duration/ChronoUnit,两者混用会算错。
  • LocalDateTime 不含时区语义,跨时区存档用 Instant(UTC 时刻)+ 目标时区展示。
  • 日期运算跨月/闰年由 API 兜底(如 1 月 31 日 plusMonths(1) 得 2 月 28 日),手写日历数学才是坑源。