新日期时间 API(LocalDate / DateTimeFormatter)

Java 8 之前,Date 和 Calendar 是所有 Java 开发者的噩梦。又绕又线程不安全,月份从 0 开始算...

我当年写代码处理日期时间时,总是需要 google "Java 获取当前日期的三种方法",因为记不住那些 API。

Java 8 引入的 java.time 包,终于把这个痛点解决了。今天我们就来彻底掌握这套新 API。

一、为什么要用新 API

1.1 Date 和 Calendar 的问题

// ❌ Date 的问题
Date date = new Date(2024, 1, 1);  // 月份从 0 开始!2024年2月1日
date.setYear(123);  // 要 +1900,实际是 2023
date.setMonth(5);   // 实际是 6 月,不是 5 月

// ❌ Calendar 的问题
Calendar cal = Calendar.getInstance();
cal.set(2024, Calendar.JANUARY, 1);  // 还要用常量
cal.add(Calendar.DAY_OF_MONTH, 7);    // 字符串常量,容易写错

// ❌ 线程不安全
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
// 多个线程同时使用会出问题

// ❌ 可变性
Date now = new Date();
now.setTime(0);  // 静默修改了!

1.2 java.time 的优势

特性Date/Calendarjava.time
线程安全是(不可变对象)
API 简洁复杂直观
月份起始01(正常)
不可变
时区支持

二、核心类型

2.1 Instant:时间戳

// Instant:瞬间点,类似 Date,但更精确
Instant now = Instant.now();
Instant epoch = Instant.EPOCH;  // 1970-01-01 00:00:00 UTC

Instant.ofEpochSecond(0);       // 1970-01-01 00:00:00
Instant.ofEpochMilli(0);        // 同上,毫秒版

// 与 Date 互转
Date date = Date.from(now);
Instant instant = date.toInstant();

// 获取时间戳
long epochSecond = now.getEpochSecond();       // 秒
long toEpochMilli = now.toEpochMilli();       // 毫秒

2.2 LocalDate:日期

// LocalDate:日期,只有 年-月-日,不包含时间
LocalDate today = LocalDate.now();  // 当前日期
LocalDate date = LocalDate.of(2024, 1, 15);  // 2024年1月15日
LocalDate date2 = LocalDate.of(2024, Month.JANUARY, 15);  // 用枚举更清晰

// 解析
LocalDate parsed = LocalDate.parse("2024-01-15");  // 默认 ISO 格式

// 常用方法
LocalDate date = LocalDate.of(2024, 1, 15);
date.getYear();              // 2024
date.getMonth();             // JANUARY
date.getMonthValue();        // 1
date.getDayOfMonth();        // 15
date.getDayOfWeek();         // MONDAY
date.getDayOfYear();         // 15(年的第15天)

// 计算
LocalDate tomorrow = today.plusDays(1);
LocalDate nextWeek = today.plusWeeks(1);
LocalDate nextMonth = today.plusMonths(1);
LocalDate nextYear = today.plusYears(1);

LocalDate yesterday = today.minusDays(1);

2.3 LocalTime:时间

// LocalTime:时间,只有 时:分:秒,不包含日期
LocalTime now = LocalTime.now();
LocalTime time = LocalTime.of(14, 30);           // 14:30
LocalTime time2 = LocalTime.of(14, 30, 15);     // 14:30:15
LocalTime time3 = LocalTime.of(14, 30, 15, 123); // 14:30:15.000000123

// 解析
LocalTime parsed = LocalTime.parse("14:30:15");

// 常用方法
time.getHour();      // 14
time.getMinute();    // 30
time.getSecond();    // 15
time.getNano();      // 123

2.4 LocalDateTime:日期时间

// LocalDateTime:日期 + 时间,不包含时区
LocalDateTime now = LocalDateTime.now();
LocalDateTime dt = LocalDateTime.of(2024, 1, 15, 14, 30);
LocalDateTime dt2 = LocalDateTime.of(date, time);

// 计算
LocalDateTime later = now.plusHours(2);
LocalDateTime earlier = now.minusDays(1);

// LocalDate + LocalTime → LocalDateTime
LocalDateTime combined = date.atTime(14, 30);
LocalDateTime combined2 = time.atDate(date);

// LocalDateTime 分解
LocalDate date = dt.toLocalDate();
LocalTime time = dt.toLocalTime();

2.5 ZonedDateTime:带时区

// ZonedDateTime:日期 + 时间 + 时区
ZonedDateTime now = ZonedDateTime.now();
ZonedDateTime zoned = ZonedDateTime.of(2024, 1, 15, 14, 30, 0, 0, ZoneId.of("Asia/Shanghai"));

// 获取时区
ZoneId zone = now.getZone();
ZoneOffset offset = now.getOffset();  // +08:00

// 常用时区
ZonedDateTime utc = ZonedDateTime.now(ZoneOffset.UTC);
ZonedDateTime tokyo = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));
ZonedDateTime ny = ZonedDateTime.now(ZoneId.of("America/New_York"));

// 时区转换
ZonedDateTime shanghai = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
ZonedDateTime utcConverted = shanghai.withZoneSameInstant(ZoneOffset.UTC);

三、Duration 和 Period

3.1 Duration:时间差(基于时间)

// Duration:基于时间(秒、纳秒),适合计算时间点之间的差
Duration duration = Duration.ofHours(2);
Duration duration2 = Duration.between(
    LocalTime.of(10, 0),
    LocalTime.of(12, 30)
);

duration.toSeconds();     // 秒数
duration.toMinutes();     // 分钟数
duration.toHours();       // 小时数
duration.toDays();        // 天数

// 适合场景:Duration.between(Instant, Instant)
// Duration.between(LocalDateTime, LocalDateTime) 也可以

3.2 Period:时间差(基于日期)

// Period:基于日期(年、月、日),适合计算日期之间的差
Period period = Period.ofDays(7);
Period period2 = Period.between(
    LocalDate.of(2024, 1, 1),
    LocalDate.of(2024, 12, 31)
);

period.getYears();    // 0
period.getMonths();   // 11
period.getDays();     // 30

period.toTotalMonths();  // 11 * 30 + 30 = 360(总月数,不精确)

3.3 Duration vs Period

类型基础适用场景示例
Duration时间测量时长"会议开了2小时"
Period日期日期加减"3个月后到期"
// Duration:时间测量
Duration.ofHours(2);  // 2 小时
Duration.ofMinutes(30);  // 30 分钟

// Period:日期计算
Period.ofMonths(3);  // 3 个月
Period.ofYears(1);   // 1 年

四、DateTimeFormatter:格式化

4.1 内置格式

LocalDateTime dt = LocalDateTime.of(2024, 1, 15, 14, 30, 45);

// 内置格式
DateTimeFormatter.ISO_LOCAL_DATE        // 2024-01-15
DateTimeFormatter.ISO_LOCAL_TIME         // 14:30:45
DateTimeFormatter.ISO_LOCAL_DATE_TIME   // 2024-01-15T14:30:45
DateTimeFormatter.ISO_OFFSET_DATE_TIME   // 2024-01-15T14:30:45+08:00

// 格式化
String formatted = dt.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
// "2024-01-15T14:30:45"

4.2 自定义格式

LocalDateTime dt = LocalDateTime.of(2024, 1, 15, 14, 30, 45);

// 自定义格式
DateTimeFormatter f1 = DateTimeFormatter.ofPattern("yyyy-MM-dd");
String dateStr = dt.format(f1);  // "2024-01-15"

DateTimeFormatter f2 = DateTimeFormatter.ofPattern("HH:mm:ss");
String timeStr = dt.format(f2);  // "14:30:45"

DateTimeFormatter f3 = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss");
String fullStr = dt.format(f3);  // "2024年01月15日 14:30:45"

DateTimeFormatter f4 = DateTimeFormatter.ofPattern("EEEE", Locale.CHINESE);
String weekStr = dt.format(f4);  // "星期一"

4.3 格式模式字母

字母含义示例
yyyyy=2024, yy=24
MMM=01, M=1
ddd=15, d=15
H小时(0-23)HH=14
h小时(1-12)hh=02
m分钟mm=30
sss=45
S毫秒SSS=123
E星期E=周一, EEEE=星期一
aAM/PMa=下午
z时区名z= CST
Z时区偏移Z=+0800

4.4 带时区的格式化

ZonedDateTime zdt = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));

// 带偏移
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss Z")
    .format(zdt);  // "2024-01-15 14:30:45 +0800"

// 带时区名
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z")
    .format(zdt);  // "2024-01-15 14:30:45 CST"

4.5 解析

// 解析字符串到日期时间
String dateStr = "2024-01-15";
LocalDate date = LocalDate.parse(dateStr);  // 使用 ISO 格式

String customStr = "2024/01/15";
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
LocalDate date2 = LocalDate.parse(customStr, formatter);

// 解析带时区的
String zonedStr = "2024-01-15T14:30:45+08:00";
ZonedDateTime zdt = ZonedDateTime.parse(zonedStr);

五、【直观类比】

【直观类比】

新日期时间 API 的四个核心类型:

类型比喻示例
Instant地球上的精确时刻2024-01-15 06:30:45 UTC
LocalDateTime日历上的日期时间(没有时区)2024年1月15日 14:30
ZonedDateTime某个时区的日期时间北京时间 2024-01-15 14:30
Duration闹钟的计时3小时30分钟
Period日历上的跨度3个月

六、生产避坑

6.1 ❌ 错误示范:混淆 LocalDateTime 和 ZonedDateTime

// ❌ 错误:存储本地时间,但用户在不同时区看到不同时间
LocalDateTime dt = LocalDateTime.now();  // 当前时区的"14:30"
saveToDb(dt);

// 用户在纽约看:纽约是"01:30"(差13小时)!

// ✅ 正确:用 ZonedDateTime 存储
ZonedDateTime zdt = ZonedDateTime.now();  // 带时区信息
saveToDb(zdt);

// 或者存储 UTC
Instant utc = Instant.now();  // UTC 时间
saveToDb(utc);

6.2 ❌ 错误示范:跨年/跨月计算错误

LocalDate date1 = LocalDate.of(2024, 1, 31);
LocalDate date2 = date1.plusMonths(1);  // 2024-02-29(不是 2月31日)
// ✅ 正确:Java 会自动处理,不会报错

6.3 ❌ 错误示范:使用 SimpleDateFormat

// ❌ 不要用 SimpleDateFormat(线程不安全)
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");

// ✅ 应该用 DateTimeFormatter(线程安全)
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
String result = LocalDate.now().format(formatter);

七、实战案例

7.1 计算两个日期之间的天数

LocalDate start = LocalDate.of(2024, 1, 1);
LocalDate end = LocalDate.of(2024, 12, 31);

long days = start.until(end).getDays();           // ❌ 只算天数部分
long totalDays = ChronoUnit.DAYS.between(start, end);  // ✅ 正确的总天数
long totalWeeks = ChronoUnit.WEEKS.between(start, end);
long totalMonths = ChronoUnit.MONTHS.between(start, end);

7.2 判断日期是否在范围内

LocalDate today = LocalDate.now();
LocalDate start = LocalDate.of(2024, 1, 1);
LocalDate end = LocalDate.of(2024, 12, 31);

// 判断是否在 [start, end] 区间内
boolean inRange = !today.isBefore(start) && !today.isAfter(end);
// 或者
boolean inRange2 = today.compareTo(start) >= 0 && today.compareTo(end) <= 0;

7.3 获取本月第一天/最后一天

LocalDate today = LocalDate.now();

// 本月第一天
LocalDate firstDay = today.withDayOfMonth(1);

// 本月最后一天
LocalDate lastDay = today.with(TemporalAdjusters.lastDayOfMonth());

// 下个月第一天
LocalDate nextMonthFirst = today.plusMonths(1).withDayOfMonth(1);

// 本季度第一天
LocalDate quarterFirst = today.withMonth(((today.getMonthValue() - 1) / 3) * 3 + 1)
    .withDayOfMonth(1);

八、面试追问链

第一层:基础用法

面试官问:"LocalDate、LocalTime、LocalDateTime 有什么区别?"

LocalDate 只有日期(年月日),LocalTime 只有时间(时分秒),LocalDateTime 是两者组合。都不带时区信息,适合表示本地时间。

第二层:时区处理

面试官追问:"ZonedDateTime 和 LocalDateTime 的区别是什么?"

LocalDateTime 不带时区,是"日历上的时间";ZonedDateTime 带时区,是"某个时区的具体时间"。存储到数据库或传输时应该用 ZonedDateTime 或 Instant,避免时区丢失。

第三层:格式化

面试官追问:"DateTimeFormatter 怎么用?为什么比 SimpleDateFormat 好?"

DateTimeFormatter 使用 ofPattern() 创建格式化器,format() 格式化日期,parse() 解析字符串。它是线程安全的,而 SimpleDateFormat 不是。

【学习小结】

  • Instant:时间戳,适合存储和计算
  • LocalDate:日期,LocalTime:时间,LocalDateTime:两者组合
  • ZonedDateTime:带时区,Instant:UTC 时间戳
  • Duration:时间差(秒),Period:日期差(年月日)
  • DateTimeFormatter:格式化,线程安全
  • 不要混淆 LocalDateTime 和 ZonedDateTime