java.time 核心类:LocalDate 与 LocalDateTime
本教程共 100 篇 · 第 62 篇 · 更新于 2026-08-05 · 约 6 分钟阅读
本节目标:掌握 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
反过来,LocalDate 用 atTime 补上时刻,LocalTime 用 atDate 补上日期,这就是前面表里 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 会就地修改。它不会。所有 plus、minus、with 开头的方法都返回新对象,不接住就等于白调。
不可变带来两个好处:一是天然线程安全,多个线程共用一个对象也不会互相干扰;二是可以放心把对象传给别的方法,不用担心被偷偷改掉。
链式调用
正因为每步都返回新对象,方法可以一路串下去:
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()); // 下月一号
要精确改单个字段,用 withYear、withMonth、withDayOfMonth、withHour 这些,语义比通用的 with 更直白。
闰年、月长与截断
d.isLeapYear(); // 是否闰年
d.lengthOfMonth(); // 当月天数,2 月平年 28、闰年 29
d.lengthOfYear(); // 当年天数 365 或 366
要把精度砍到某一级,用 truncatedTo。做按小时聚合的统计时很常用:
dt.truncatedTo(java.time.temporal.ChronoUnit.HOURS); // 分、秒、纳秒全部清零
比较两个日期
判断先后用 isBefore、isAfter、isEqual:
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:基于时间,精确到秒和纳秒,配LocalTime、LocalDateTime、Instant。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没有时间部分,运行时会抛异常。记牢配对关系:LocalDate配Period或ChronoUnit.DAYS,带时刻的配Duration。
常见疑问
问:plusDays 明明调用了,日期怎么没变?
因为返回值没接住。所有 plus、minus、with 都返回新对象,原对象永远不变。写成 d = d.plusDays(1) 或者用一个新变量接住才有效。这是新手第一大坑。
问:LocalDateTime 和 Instant 到底选哪个?
看这个时间要不要跨时区。存进数据库、传给别的服务、算两次操作的耗时,都用 Instant,它全球统一不会歧义。只在本地展示、或者本身就与时区无关(比如”每天 9 点上班”),用 LocalDateTime 或 LocalTime。
问:Period.between 算出来是负数正常吗?
正常。如果第一个参数比第二个晚,结果就是负的。想要绝对值,先用 isBefore 判断一下顺序,或者调 Period 的 normalized() 之后自己取正。
小结
LocalDate、LocalTime、LocalDateTime 管本地钟面时间,Instant 管绝对时刻,各司其职。
方法名有统一前缀规律:of 造、parse 解析、now 取当前、get 读、with 改、plus 和 minus 加减、to 转换、at 组合,没有 set。
对象全都不可变,任何修改都返回新对象,必须接住返回值;也正因如此可以链式调用,且天然线程安全。
比先后用 isBefore 和 isAfter;算间隔时 LocalDate 配 Period,带时刻的配 Duration,要总数用 ChronoUnit。