首页 / Java 入门教程 / java.time 核心类:LocalDate 与 LocalDateTime

Java 入门教程

java.time 核心类:LocalDate 与 LocalDateTime

本教程共 100 篇 · 第 62 篇 · 更新于 2026-08-05 · 约 6 分钟阅读

JavaJava 入门教程java.timeLocalDateLocalDateTimeDuration

本节目标:掌握 LocalDate、LocalTime、LocalDateTime、Instant 的创建、修改、加减和比较,分清 Duration 和 Period,理解”不可变”到底意味着什么。

上一章说清了为什么要用新 API,这一章直接上手。最常用的三个类是 LocalDate(日期)、LocalTime(时间)、LocalDateTime(日期+时间)。名字里的 Local 指”本地”,意思是不带时区,相当于挂在墙上的日历和钟表。

先看职责分工表

java.time 类不少,初学最容易迷路。先记住这张表,遇到需求直接查:

表示什么带时区吗典型用途
LocalDate年月日生日、入职日期、账单日
LocalTime时分秒每天的闹钟、营业时间
LocalDateTime年月日 + 时分秒会议开始时间、日志时间
Instant时间线上的绝对时刻是(固定 UTC)存数据库、算耗时、传接口
ZonedDateTime带完整时区规则的时间跨时区显示、含夏令时计算
Duration一段时长(时分秒)两个时刻相差多久
Period一段日期跨度(年月日)两个日期相差几年几月几天

选类的口诀:只关心”哪天”用 LocalDate;关心”哪一瞬间”用 Instant;要显示给不同时区的人看,用 ZonedDateTime

命名规律:学一个会一片

新 API 的方法名是统一设计过的,前缀就代表功能。记住这七个前缀,几十个类的方法都能猜出来:

前缀类型作用
of静态工厂用给定参数直接造一个实例
from静态工厂从另一种类型转换过来
parse静态工厂解析字符串得到实例
now静态工厂取当前的日期或时间
get实例方法读取对象里的某个字段
is实例方法查询状态,返回布尔值
with实例方法改一个字段,返回新对象
plus / minus实例方法加上或减去一段时间
to实例方法转成另一种类型
at实例方法和另一个对象组合起来

注意表里没有 set。因为对象都不可变,改值这件事由 with 承担,它不改原对象,而是返回一个改过的副本。

LocalDate:只管年月日

三种创建方式,覆盖绝大多数场景:

import java.time.LocalDate;

LocalDate today = LocalDate.now();                // 取运行当天,如 2026-08-05
LocalDate d = LocalDate.of(2026, 8, 5);           // 手动指定,月份就写 8
LocalDate parsed = LocalDate.parse("2026-08-05"); // 按 ISO 标准格式解析

逐行看:now() 读系统时钟和默认时区;of() 直接给年月日,月份是 1~12,写 8 就是八月;parse() 默认认 ISO 8601 的 yyyy-MM-dd,不用自己写解析规则。

取其中的字段也很直观:

d.getYear();        // 2026
d.getMonthValue();  // 8,注意这里是 1~12,不再是 0 起了
d.getMonth();       // AUGUST,返回的是枚举 Month
d.getDayOfMonth();  // 5
d.getDayOfWeek();   // WEDNESDAY,返回枚举 DayOfWeek
d.getDayOfYear();   // 217,今年的第几天
Tip

新 API 的月份终于从 1 开始了,getMonthValue() 返回 1~12,不用再加加减减。这正是它比旧 Calendar 贴心的地方,也是迁移老代码时最容易漏改的地方——原来的 +1 记得删掉。

参数写错会立刻报错,不会默默算成别的日期:

LocalDate bad = LocalDate.of(2026, 2, 30);
// 抛 DateTimeException: Invalid date 'FEBRUARY 30'

Calendar 遇到 2 月 30 日会悄悄滚成 3 月 2 日,错误被藏起来;新 API 直接抛异常,错在哪一眼就看到。

LocalTime:只管时刻

import java.time.LocalTime;

LocalTime now = LocalTime.now();            // 当前时刻
LocalTime t = LocalTime.of(14, 30);         // 14:30
LocalTime precise = LocalTime.of(14, 30, 5);// 14:30:05,可带秒
LocalTime t2 = LocalTime.parse("14:30:00"); // 解析

LocalTime 不含日期,适合表示”每天固定的闹钟时间""营业开始时间”这类与具体哪天无关的时刻。它还提供了几个常量:LocalTime.MIDNIGHT 是 00:00,LocalTime.NOON 是 12:00。

LocalDateTime:日期加时间

把上面两个合起来,就是 LocalDateTime

import java.time.LocalDateTime;

LocalDateTime dt = LocalDateTime.of(2026, 8, 5, 14, 30);
LocalDateTime now = LocalDateTime.now();
LocalDateTime combined = LocalDateTime.of(d, t);  // 由 LocalDate + LocalTime 拼成

它既能取日期部分也能取时间部分,toLocalDate()toLocalTime() 随时拆开:

LocalDate date = dt.toLocalDate();   // 2026-08-05
LocalTime time = dt.toLocalTime();   // 14:30

反过来,LocalDateatTime 补上时刻,LocalTimeatDate 补上日期,这就是前面表里 at 前缀的含义:

LocalDateTime a = d.atTime(14, 30);      // 日期补时刻
LocalDateTime b = d.atStartOfDay();      // 补成当天 00:00

不可变:改了会返回新对象

这是新 API 最关键的性质,务必看懂下面这段:

LocalDate d = LocalDate.of(2026, 8, 5);
d.plusDays(1);                 // 返回值被丢掉了,d 一点没变
System.out.println(d);         // 仍然是 2026-08-05

LocalDate next = d.plusDays(1);   // 正确:接住返回值
System.out.println(next);         // 2026-08-06
System.out.println(d);            // 2026-08-05,原对象依旧不变

初学者最常犯的错就是第二行那样,以为 plusDays 会就地修改。它不会。所有 plusminuswith 开头的方法都返回新对象,不接住就等于白调。

不可变带来两个好处:一是天然线程安全,多个线程共用一个对象也不会互相干扰;二是可以放心把对象传给别的方法,不用担心被偷偷改掉。

链式调用

正因为每步都返回新对象,方法可以一路串下去:

LocalDateTime result = LocalDateTime.now()
        .plusDays(7)          // 加 7 天
        .withHour(9)          // 小时改成 9
        .withMinute(0)        // 分钟改成 0
        .truncatedTo(java.time.temporal.ChronoUnit.MINUTES); // 秒和纳秒清零

读起来像一句英文:取当前时间,加七天,把时刻定到 9 点整。这就是链式 API 的价值——每一步意图清楚,不需要中间变量。

加减与调整

加减有成套方法,单位齐全:

d.plusDays(3);      // 加三天
d.plusWeeks(2);     // 加两周
d.minusMonths(1);   // 减一个月
d.plusYears(1);     // 加一年
dt.plusHours(6);    // 加六小时,LocalDateTime 才有

跨月加减会自动处理边界。1 月 31 日加一个月,得到的是 2 月 28 日(闰年 29 日),而不是不存在的 2 月 31 日:

LocalDate.of(2026, 1, 31).plusMonths(1);  // 2026-02-28,自动收到当月最后一天

复杂调整交给 TemporalAdjusters,它是一组现成的”调整器”:

import java.time.temporal.TemporalAdjusters;
import java.time.DayOfWeek;

d.with(TemporalAdjusters.firstDayOfMonth());     // 当月第一天
d.with(TemporalAdjusters.lastDayOfMonth());      // 当月最后一天
d.with(TemporalAdjusters.next(DayOfWeek.MONDAY));// 下一个周一
d.with(TemporalAdjusters.firstDayOfNextMonth()); // 下月一号

要精确改单个字段,用 withYearwithMonthwithDayOfMonthwithHour 这些,语义比通用的 with 更直白。

闰年、月长与截断

d.isLeapYear();        // 是否闰年
d.lengthOfMonth();     // 当月天数,2 月平年 28、闰年 29
d.lengthOfYear();      // 当年天数 365 或 366

要把精度砍到某一级,用 truncatedTo。做按小时聚合的统计时很常用:

dt.truncatedTo(java.time.temporal.ChronoUnit.HOURS); // 分、秒、纳秒全部清零

比较两个日期

判断先后用 isBeforeisAfterisEqual

LocalDate a = LocalDate.of(2026, 8, 5);
LocalDate b = LocalDate.of(2026, 9, 1);
System.out.println(a.isBefore(b)); // true
System.out.println(a.isAfter(b));  // false
System.out.println(a.equals(b));   // false
Warning

别用 >< 去比日期对象,那是给基本类型用的,对象之间根本不能这么比,编译就通不过。compareTo 也能用,返回负数、0、正数,但可读性远不如 isBefore

Instant:绝对时刻

Instant 和上面几个不一样,它不是”钟面时间”,而是时间线上的一个绝对点,底层就是从 1970 纪元算起的秒数加纳秒数。

import java.time.Instant;

Instant now = Instant.now();
long ms = now.toEpochMilli();                  // 转成毫秒时间戳
Instant back = Instant.ofEpochMilli(ms);       // 从时间戳还原

算一段代码跑了多久,就该用它,因为它不受时区和系统时间调整的影响:

Instant start = Instant.now();
// ... 一些耗时操作 ...
Instant end = Instant.now();
System.out.println(Duration.between(start, end).toMillis() + " 毫秒");

Duration 与 Period:算间隔

算两个时间点差多少,要看是”基于时间”还是”基于日期”:

  • Duration:基于时间,精确到秒和纳秒,配 LocalTimeLocalDateTimeInstant
  • Period:基于日期,按年月日算,配 LocalDate
import java.time.Duration;
import java.time.Period;

Duration dur = Duration.between(
        LocalDateTime.of(2026, 8, 5, 14, 0),
        LocalDateTime.of(2026, 8, 5, 16, 30));
System.out.println(dur.toMinutes());   // 150

Period p = Period.between(
        LocalDate.of(2026, 1, 1),
        LocalDate.of(2026, 8, 5));
System.out.println(p.getMonths());     // 7
System.out.println(p.getDays());       // 4

Period 有个容易看错的地方:getMonths() 返回的是”零头月数”,不是总月数。跨年时 getYears()getMonths() 要一起看,比如相差 1 年 3 个月,getYears() 是 1、getMonths() 是 3,不是 15。

想直接拿”总共差多少天”,用 ChronoUnit

import java.time.temporal.ChronoUnit;

long days = ChronoUnit.DAYS.between(a, b);      // 总天数
long months = ChronoUnit.MONTHS.between(a, b);  // 总月数
Warning

别拿 Duration 去算两个 LocalDate 的间隔,LocalDate 没有时间部分,运行时会抛异常。记牢配对关系:LocalDatePeriodChronoUnit.DAYS,带时刻的配 Duration

常见疑问

问:plusDays 明明调用了,日期怎么没变?

因为返回值没接住。所有 plusminuswith 都返回新对象,原对象永远不变。写成 d = d.plusDays(1) 或者用一个新变量接住才有效。这是新手第一大坑。

问:LocalDateTimeInstant 到底选哪个?

看这个时间要不要跨时区。存进数据库、传给别的服务、算两次操作的耗时,都用 Instant,它全球统一不会歧义。只在本地展示、或者本身就与时区无关(比如”每天 9 点上班”),用 LocalDateTimeLocalTime

问:Period.between 算出来是负数正常吗?

正常。如果第一个参数比第二个晚,结果就是负的。想要绝对值,先用 isBefore 判断一下顺序,或者调 Periodnormalized() 之后自己取正。

小结

LocalDateLocalTimeLocalDateTime 管本地钟面时间,Instant 管绝对时刻,各司其职。

方法名有统一前缀规律:of 造、parse 解析、now 取当前、get 读、with 改、plusminus 加减、to 转换、at 组合,没有 set

对象全都不可变,任何修改都返回新对象,必须接住返回值;也正因如此可以链式调用,且天然线程安全。

比先后用 isBeforeisAfter;算间隔时 LocalDatePeriod,带时刻的配 Duration,要总数用 ChronoUnit