Java时间新API(java.time)

ℹ️本篇定位

JDK 8 引入的 java.time 彻底替代了 Date/Calendar/SimpleDateFormat。老 API 三大罪:月份从 0 开始、线程不安全、格式化有坑。新 API 一律不可变+线程安全+清晰命名。本篇不重述老 API 用法(鱼皮 18-Java 日期时间 已覆盖入门),只讲新 API 的"为什么好"和"怎么用"。

一、问题提出:老 API 的三个经典坑

// 坑1:月份从 0 开始——12 月 new Date(2024, 11, 31) 写成 12 就炸
Date d = new Date(2024, 11, 31);   // 实际是 2025 年 12 月 31 日,因为 year 是 1900+124

// 坑2:SimpleDateFormat 线程不安全,多线程下 format 会乱序甚至抛异常
static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 多线程调 sdf.format(...) 崩

// 坑3:Calendar 的 set 之后不立刻生效,要 get 一下才重算(内部字段惰性)
Calendar c = Calendar.getInstance();
c.set(Calendar.MONTH, 13);         // 不报错,自动进位到明年 2 月——不报错反而不安全

二、新 API 核心类:三句话记住所有类

表示什么 关键方法
LocalDate 日期(年月日) now(), of(2024,12,31), plusDays(7), isBefore/After
LocalTime 时间(时分秒纳秒) now(), of(10,30,0), plusHours(2), isBefore/After
LocalDateTime 日期+时间(无时区) now(), of(date,time), plusDays(1), toLocalDate()
ZonedDateTime 日期+时间+时区 now(ZoneId.of("Asia/Shanghai")), withZoneSameInstant()
Instant 时间戳(UTC 毫秒/纳秒) now(), toEpochMilli(), plusSeconds(3600)
Duration 时间差(时分秒) between(t1,t2), toHours()
Period 日期差(年月日) between(d1,d2), getYears(), getMonths()

两条铁律:① 所有新 API 类都是 不可变的——plusDays 返回新对象,原对象不动(没有副作用,天然线程安全);② 数据库存时间戳用 Instant,业务展示用 LocalDateTime+时区,跨时区转换走 ZonedDateTime

LocalDate today = LocalDate.now();
LocalDate nextWeek = today.plusDays(7);           // 不可变,返回新对象
LocalDate birthday = LocalDate.of(2000, 1, 15);   // 1 月就是 1,不是 0
Period age = Period.between(birthday, today);
System.out.println(age.getYears());               // 直接拿年数,不用手算

// 跨时区:北京时间 → 纽约时间
ZonedDateTime bj = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
ZonedDateTime ny = bj.withZoneSameInstant(ZoneId.of("America/New_York"));

三、格式化:DateTimeFormatter 替代 SimpleDateFormat

DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime now = LocalDateTime.now();
String s = now.format(fmt);                     // 格式化
LocalDateTime parsed = LocalDateTime.parse(s, fmt);  // 解析

线程安全DateTimeFormatter 不可变,可以放心存成 static final 多线程共用——这是和 SimpleDateFormat 的最大区别。

预定义格式:

DateTimeFormatter.ISO_LOCAL_DATE;         // 2024-12-31
DateTimeFormatter.ISO_LOCAL_DATE_TIME;    // 2024-12-31T10:30:00
DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL);  // 根据 Locale 本地化

四、新旧 API 互转

// Date → Instant → LocalDateTime
Date old = new Date();
LocalDateTime ldt = old.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();

// LocalDateTime → Instant → Date
Date back = Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant());

转换的核心是 Instant——它是 UTC 时间戳的通用表示,两边都认。

五、常用操作速查

// 比较
localDate.isBefore(other);  localDate.isAfter(other);  localDate.isEqual(other);

// 调整
localDate.withDayOfMonth(1);           // 当月第一天
localDate.with(TemporalAdjusters.lastDayOfMonth());  // 当月最后一天
localDate.with(TemporalAdjusters.next(DayOfWeek.MONDAY));  // 下个周一

// 计算
Duration.between(time1, time2).toMinutes();   // 分钟差
Period.between(date1, date2).getDays();        // 日期差(注意跨月不算自然天)
ChronoUnit.DAYS.between(date1, date2);         // 自然天数差(推荐)

// 时区
ZoneId.of("Asia/Shanghai");                    // 永远用"地区/城市"格式,不用 UTC+8 偏移
ZoneId.systemDefault();                        // 服务器默认时区

六、高频面试题速答

  1. Date 和 LocalDate 选哪个? 新项目一律用 java.time;Date 只用于老代码互转、数据库驱动兼容层。
  2. SimpleDateFormat 为什么线程不安全? 内部 Calendar 字段是共享可变状态,多线程 format 会互相覆盖——用 DateTimeFormatter 或每次 new 一个。
  3. LocalDateTime 和 ZonedDateTime 区别? 前者是"墙上时钟"(不携带时区信息),后者带了时区。存数据库优先用 Instant(时间戳),展示用 LocalDateTime+前端时区。
  4. Period 和 Duration 区别? Period 是日期差(年月日),Duration 是时间差(时分秒纳秒)。Period.between(2024-01-01, 2024-12-31) 返回 11 个月 30 天,不是 365 天——算自然天数用 ChronoUnit.DAYS.between

勾连


⬅️ 03-BigDecimal 🏠 00-Java ➡️ 05-Comparable与Comparator