日期时间 API
java.time:Local/Zoned/Instant、Duration vs Period、格式化与解析
为什么不用 Date / Calendar
Java 8 之前的 Date / Calendar 是历史包袱:可变(线程不安全)、月份从 0 开始、设计混乱。新项目一律用 java.time(JSR-310,借鉴 Joda-Time):不可变、线程安全、API 清晰。下文所有示例都基于 java.time。
- 应用场景:用户注册时间、订单倒计时、定时任务调度、跨时区会议、计费周期
- 核心类速记:LocalDate(日期)/ LocalTime(时间)/ LocalDateTime(无时区日期+时间)/ ZonedDateTime(带时区)/ Instant(epoch 时间戳)/ Duration(时间长度,秒/纳秒)/ Period(年月日长度)
LocalDate / LocalTime / LocalDateTime
三个"不带时区"的类:LocalDate 只有日期、LocalTime 只有时间、LocalDateTime 两者都有。适合表达"业务日期"(生日、营业时间),与具体时区无关。
// LocalDemo.java
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Month;
public class LocalDemo {
public static void main(String[] args) {
LocalDate today = LocalDate.now();
LocalDate birthday = LocalDate.of(1990, Month.MAY, 12);
System.out.println(today + " " + birthday);
LocalTime noon = LocalTime.of(12, 0);
LocalTime now = LocalTime.now();
System.out.println(noon + " " + now);
LocalDateTime meeting = LocalDateTime.of(2026, 5, 12, 14, 30);
System.out.println(meeting);
// 加减:返回新对象
System.out.println(today.plusDays(7));
System.out.println(today.minusMonths(1));
System.out.println(today.withDayOfMonth(1)); // 本月 1 号
// 字段访问
System.out.println("year=" + today.getYear() + " month=" + today.getMonthValue() + " day=" + today.getDayOfMonth());
System.out.println("weekday=" + today.getDayOfWeek());
}
}ZonedDateTime 与时区
应用场景:要展示"上海现在几点"、"伦敦同事看到的时间"、跨时区会议。ZonedDateTime = LocalDateTime + ZoneId。同一个瞬间,在不同 ZoneId 下显示的时钟数字不同。
// ZonedDemo.java
import java.time.ZoneId;
import java.time.ZonedDateTime;
public class ZonedDemo {
public static void main(String[] args) {
ZoneId sh = ZoneId.of("Asia/Shanghai");
ZoneId ny = ZoneId.of("America/New_York");
ZonedDateTime nowSh = ZonedDateTime.now(sh);
ZonedDateTime nowNy = nowSh.withZoneSameInstant(ny); // 同一瞬间,换时区
System.out.println(nowSh);
System.out.println(nowNy);
// 它们的 Instant 相等
System.out.println(nowSh.toInstant().equals(nowNy.toInstant())); // true
}
}Instant:epoch 时间戳
应用场景:和外部系统传值、写入数据库的时间戳列、日志排序、计算时间差。Instant 是"距 1970-01-01 UTC 的纳秒数"——无时区、绝对的瞬间。
// InstantDemo.java
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
public class InstantDemo {
public static void main(String[] args) {
Instant now = Instant.now();
System.out.println(now); // 2026-05-12T08:30:00.123Z
System.out.println(now.toEpochMilli()); // Unix 毫秒
System.out.println(now.getEpochSecond()); // Unix 秒
// Instant <-> ZonedDateTime
ZonedDateTime sh = now.atZone(ZoneId.of("Asia/Shanghai"));
System.out.println(sh);
// 从 epoch 毫秒还原
Instant fromMs = Instant.ofEpochMilli(1_715_000_000_000L);
System.out.println(fromMs);
}
}Duration vs Period
Duration 表达"基于秒/纳秒的精确时间长度"(5 小时、200 毫秒);Period 表达"日历层面的年月日长度"(2 个月、3 年)。两者不可互换:"一个月"长度不固定(28~31 天),不能用 Duration 表达。
// DurationPeriod.java
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.Period;
public class DurationPeriod {
public static void main(String[] args) {
// Duration:秒/纳秒
Duration d = Duration.ofMinutes(90).plusSeconds(30);
System.out.println(d); // PT1H30M30S
System.out.println(d.toMinutes() + "m"); // 90m
LocalTime t1 = LocalTime.of(9, 0);
LocalTime t2 = LocalTime.of(17, 30);
Duration worked = Duration.between(t1, t2);
System.out.println(worked.toHours() + "h");
// Period:年月日
Period p = Period.of(2, 6, 0); // 2 年 6 月
LocalDate from = LocalDate.of(2020, 1, 1);
LocalDate to = LocalDate.of(2026, 5, 12);
Period age = Period.between(from, to);
System.out.printf("%d年%d月%d天%n", age.getYears(), age.getMonths(), age.getDays());
}
}DateTimeFormatter:格式化与解析
应用场景:前后端 API 时间字段、日志时间戳、CSV/Excel 导出。提供常用格式(ISO_DATE / ISO_DATE_TIME / RFC_1123)和自定义 pattern。**与 SimpleDateFormat 不同,DateTimeFormatter 是线程安全的,可以做 static final 字段全局共享**。
// FormatDate.java
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class FormatDate {
static final DateTimeFormatter CN = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm");
public static void main(String[] args) {
LocalDateTime now = LocalDateTime.of(2026, 5, 12, 14, 30);
// 标准 ISO
System.out.println(now.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
// 自定义
System.out.println(now.format(CN));
// 简写
System.out.println(now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
// 解析
LocalDate d = LocalDate.parse("2026-05-12");
System.out.println(d);
LocalDateTime dt = LocalDateTime.parse(
"2026-05-12 14:30",
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
);
System.out.println(dt);
}
}ChronoUnit:精确单位计算
应用场景:算两个日期间的天数 / 小时数 / 月数。ChronoUnit.X.between(a, b) 比手算 Duration / Period 字段更清晰。
// ChronoBetween.java
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
public class ChronoBetween {
public static void main(String[] args) {
LocalDate signup = LocalDate.of(2024, 3, 1);
LocalDate today = LocalDate.of(2026, 5, 12);
System.out.println(ChronoUnit.DAYS.between(signup, today)); // 总天数
System.out.println(ChronoUnit.MONTHS.between(signup, today)); // 26
LocalDateTime start = LocalDateTime.of(2026, 5, 12, 9, 0);
LocalDateTime end = LocalDateTime.of(2026, 5, 12, 14, 30);
System.out.println(ChronoUnit.MINUTES.between(start, end)); // 330
}
}常用日期计算
// DateMath.java
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.temporal.TemporalAdjusters;
public class DateMath {
public static void main(String[] args) {
LocalDate today = LocalDate.of(2026, 5, 12);
System.out.println(today.with(TemporalAdjusters.firstDayOfMonth())); // 月初
System.out.println(today.with(TemporalAdjusters.lastDayOfMonth())); // 月末
System.out.println(today.with(TemporalAdjusters.next(DayOfWeek.MONDAY))); // 下周一
System.out.println(today.with(TemporalAdjusters.firstDayOfNextMonth())); // 下月 1 号
// 闰年
System.out.println(today.isLeapYear());
}
}