Java日期时间API演进:从Date到LocalDateTime,该用哪个
Java日期时间API演进:从Date到LocalDateTime,该用哪个
适读人群:Java初中级开发者、对日期时间处理感到困惑的后端工程师 | 阅读时长:约14分钟 | 文章类型:演进梳理+选型指南
开篇故事
我记得有一次做代码审查,看到一个同事写了这样的代码:
Date now = new Date();
Calendar cal = Calendar.getInstance();
cal.setTime(now);
cal.add(Calendar.DAY_OF_MONTH, 7);
Date nextWeek = cal.getTime();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String formatted = sdf.format(nextWeek);5行代码,就为了得到"7天后的日期字符串"。
我当时没说话,自己打了一行:
String formatted = LocalDate.now().plusDays(7).format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));他看了看,说:"我以为LocalDateTime就是换了个名字……"
不,它不是换个名字那么简单。Java日期时间API经历了三代演进,每一代都是在修复上一代的缺陷。如果不了解这段历史,你不知道为什么那么多"坑"存在,也不知道该用哪个类。
一、三代API的演进历史
第一代:java.util.Date + java.util.Calendar(JDK 1.0/1.1)
Date类在JDK 1.0就有了,设计极其糟糕:
- 月份从0开始(0=January,11=December),极易出错
getYear()返回的是相对于1900年的偏移(2024年返回124)- 大量方法在JDK 1.1就被标记为@Deprecated
- 可变对象,线程不安全
- 没有时区概念
因为Date太难用,JDK 1.1加入了Calendar来弥补,但Calendar同样有问题:
- API设计繁琐(看上面那5行代码)
- 可变对象,线程不安全
- 月份仍然从0开始
- 没有日期和时间的分离概念
第二代:Joda-Time(第三方库,2002年)
Java官方的日期API太难用,社区忍无可忍,出现了Joda-Time。它引入了:
- 不可变对象
- 清晰的时区处理
- 流畅的链式API
- 分离的日期(LocalDate)、时间(LocalTime)、日期时间(LocalDateTime)概念
Joda-Time太好用了,JSR 310(Java 8的日期时间)基本就是把它的设计思想标准化了。
第三代:java.time(Java 8,JSR 310)
Java 8终于在标准库里提供了好用的日期时间API:
- LocalDate:只有日期(年月日),无时区
- LocalTime:只有时间(时分秒纳秒),无时区
- LocalDateTime:日期+时间,无时区
- ZonedDateTime:带时区的日期时间
- OffsetDateTime:带UTC偏移量的日期时间
- Instant:时间戳(从UTC 1970-01-01起的秒数+纳秒)
- Duration:时间段(以秒和纳秒表示)
- Period:日期段(以年月日表示)
- DateTimeFormatter:线程安全的格式化器
所有这些类都是不可变的,线程安全。
二、核心原理深挖
为什么需要分开Local和Zoned
很多人会问:既然有了ZonedDateTime,为什么还要LocalDateTime?
答案是:LocalDateTime不包含时区信息,更接近"人类日历上的时间"概念。
举个例子:"2024年春节是2024年2月10日"——这个日期,不管你在北京还是纽约,日历上就是2月10日,没有时区的概念。如果用ZonedDateTime表示,反而会引入不必要的复杂性。
但如果你要记录"某个事件发生的精确时刻"(比如用户下单的时间),就必须有时区概念,才能在不同时区的服务间正确比较顺序。
各类型的适用场景
Date和LocalDateTime的互转
项目中经常需要和旧代码互转:
// Date → LocalDateTime
Date date = new Date();
LocalDateTime ldt = date.toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDateTime();
// LocalDateTime → Date
LocalDateTime now = LocalDateTime.now();
Date date2 = Date.from(now.atZone(ZoneId.systemDefault()).toInstant());三、完整代码实现
代码一:java.time API常用操作速查
package com.laozhang.date.evolution;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAdjusters;
import java.util.Date;
/**
* Java 8+ 日期时间API常用操作完整示例
* 覆盖日常开发90%的使用场景
*/
public class JavaTimeApiGuide {
public static void main(String[] args) {
// ===== 1. 创建 =====
System.out.println("=== 创建 ===");
LocalDate today = LocalDate.now();
LocalTime now = LocalTime.now();
LocalDateTime nowDt = LocalDateTime.now();
Instant instant = Instant.now(); // UTC时间戳
System.out.println("今天: " + today);
System.out.println("现在: " + now);
System.out.println("现在(dt): " + nowDt);
System.out.println("时间戳: " + instant);
// 指定值创建
LocalDate birthday = LocalDate.of(1990, 8, 15); // 注意:月份1-12(不是0-11)
LocalDateTime event = LocalDateTime.of(2024, 1, 15, 10, 30, 0);
// ===== 2. 格式化与解析 =====
System.out.println("\n=== 格式化与解析 ===");
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
DateTimeFormatter dateFmt = DateTimeFormatter.ofPattern("yyyy-MM-dd");
DateTimeFormatter chineseFmt = DateTimeFormatter.ofPattern("yyyy年MM月dd日");
System.out.println("格式化: " + nowDt.format(fmt));
System.out.println("中文格式: " + today.format(chineseFmt));
LocalDateTime parsed = LocalDateTime.parse("2024-01-15 10:30:00", fmt);
System.out.println("解析: " + parsed);
// ISO 8601格式(默认)
System.out.println("ISO格式: " + nowDt.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
// ===== 3. 加减计算 =====
System.out.println("\n=== 加减计算 ===");
LocalDate nextWeek = today.plusWeeks(1);
LocalDate lastMonth = today.minusMonths(1);
LocalDateTime tomorrow10am = LocalDateTime.of(today.plusDays(1), LocalTime.of(10, 0));
System.out.println("下周今天: " + nextWeek);
System.out.println("上个月今天: " + lastMonth);
System.out.println("明天10点: " + tomorrow10am.format(fmt));
// ===== 4. 比较 =====
System.out.println("\n=== 比较 ===");
LocalDate dateA = LocalDate.of(2024, 1, 10);
LocalDate dateB = LocalDate.of(2024, 1, 15);
System.out.println("dateA.isBefore(dateB): " + dateA.isBefore(dateB)); // true
System.out.println("dateA.isAfter(dateB): " + dateA.isAfter(dateB)); // false
System.out.println("dateA.isEqual(dateA): " + dateA.isEqual(dateA)); // true
// ===== 5. 时间差计算 =====
System.out.println("\n=== 时间差 ===");
LocalDate birthDate = LocalDate.of(1990, 8, 15);
long ageInYears = ChronoUnit.YEARS.between(birthDate, today);
long daysBetween = ChronoUnit.DAYS.between(dateA, dateB);
System.out.println("年龄: " + ageInYears + "岁");
System.out.println("相差天数: " + daysBetween + "天");
// Duration:秒级时间差
LocalDateTime start = LocalDateTime.of(2024, 1, 15, 9, 0, 0);
LocalDateTime end = LocalDateTime.of(2024, 1, 15, 17, 30, 0);
Duration duration = Duration.between(start, end);
System.out.println("时长: " + duration.toHours() + "小时" + (duration.toMinutesPart()) + "分钟");
// Period:年月日时间差
Period period = Period.between(birthDate, today);
System.out.println("出生至今: " + period.getYears() + "年" + period.getMonths() + "月" + period.getDays() + "天");
// ===== 6. 常用调整器 =====
System.out.println("\n=== 常用调整器 ===");
LocalDate firstDayOfMonth = today.with(TemporalAdjusters.firstDayOfMonth());
LocalDate lastDayOfMonth = today.with(TemporalAdjusters.lastDayOfMonth());
LocalDate nextMonday = today.with(TemporalAdjusters.next(DayOfWeek.MONDAY));
LocalDate firstDayOfNextYear = today.with(TemporalAdjusters.firstDayOfNextYear());
System.out.println("本月第一天: " + firstDayOfMonth);
System.out.println("本月最后一天: " + lastDayOfMonth);
System.out.println("下周一: " + nextMonday);
System.out.println("明年元旦: " + firstDayOfNextYear);
// ===== 7. 时区处理 =====
System.out.println("\n=== 时区处理 ===");
ZonedDateTime shanghaiTime = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
ZonedDateTime newYorkTime = shanghaiTime.withZoneSameInstant(ZoneId.of("America/New_York"));
System.out.println("上海时间: " + shanghaiTime.format(fmt) + " " + shanghaiTime.getZone());
System.out.println("纽约时间: " + newYorkTime.format(fmt) + " " + newYorkTime.getZone());
// ===== 8. 与Date/long互转 =====
System.out.println("\n=== 与旧API互转 ===");
Date legacyDate = new Date();
LocalDateTime fromDate = legacyDate.toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDateTime();
Date backToDate = Date.from(LocalDateTime.now()
.atZone(ZoneId.systemDefault())
.toInstant());
// long时间戳互转
long timestamp = Instant.now().toEpochMilli();
LocalDateTime fromTimestamp = Instant.ofEpochMilli(timestamp)
.atZone(ZoneId.systemDefault())
.toLocalDateTime();
System.out.println("Date转LocalDateTime: " + fromDate.format(fmt));
System.out.println("时间戳转LocalDateTime: " + fromTimestamp.format(fmt));
}
}代码二:项目中的时间类型选型实践
package com.laozhang.date.evolution;
import java.time.*;
import java.time.format.DateTimeFormatter;
/**
* 不同场景下的时间类型选型指南
* 含实际业务场景代码
*/
public class DateTypeSelectionGuide {
// ===== 场景1:用户生日(只有日期,无时区)=====
static class UserProfile {
private final String name;
private final LocalDate birthday; // 生日:只有日期,用LocalDate
UserProfile(String name, LocalDate birthday) {
this.name = name;
this.birthday = birthday;
}
public int getAge() {
return (int) ChronoUnit.YEARS.between(birthday, LocalDate.now());
}
public boolean isBirthdayToday() {
LocalDate today = LocalDate.now();
return birthday.getMonthValue() == today.getMonthValue()
&& birthday.getDayOfMonth() == today.getDayOfMonth();
}
}
// ===== 场景2:订单创建时间(需要时区,用Instant存储)=====
static class Order {
private final String orderId;
private final Instant createdAt; // 精确时刻,用Instant存储(最适合数据库/传输)
Order(String orderId) {
this.orderId = orderId;
this.createdAt = Instant.now(); // UTC时间戳
}
public String getCreatedAtForDisplay(ZoneId userTimezone) {
// 显示时,根据用户所在时区转换
return createdAt.atZone(userTimezone)
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
public boolean isCreatedBefore(Instant other) {
return createdAt.isBefore(other);
}
}
// ===== 场景3:营业时间(只有时间段,无日期)=====
static class BusinessHours {
private final LocalTime openTime; // 开门时间
private final LocalTime closeTime; // 关门时间
BusinessHours(LocalTime open, LocalTime close) {
this.openTime = open;
this.closeTime = close;
}
public boolean isOpen(LocalTime time) {
return !time.isBefore(openTime) && !time.isAfter(closeTime);
}
public boolean isOpenNow() {
return isOpen(LocalTime.now());
}
}
// ===== 场景4:活动时间(日期+时间,无时区,本地活动)=====
static class LocalEvent {
private final String title;
private final LocalDateTime startTime; // 本地时间,不跨时区
private final LocalDateTime endTime;
LocalEvent(String title, LocalDateTime start, LocalDateTime end) {
this.title = title;
this.startTime = start;
this.endTime = end;
}
public Duration getDuration() {
return Duration.between(startTime, endTime);
}
public boolean isHappeningNow() {
LocalDateTime now = LocalDateTime.now();
return !now.isBefore(startTime) && !now.isAfter(endTime);
}
}
// ===== 场景5:数据库存储推荐 =====
static class DatabaseBestPractice {
/*
* 数据库字段类型与Java类型对应:
*
* MySQL DATETIME → LocalDateTime(不含时区)
* MySQL TIMESTAMP → Instant 或 OffsetDateTime(含时区)
* MySQL DATE → LocalDate
* MySQL TIME → LocalTime
* MySQL BIGINT(毫秒) → Instant(via toEpochMilli/ofEpochMilli)
*
* 推荐:
* - 业务时间(创建时间、更新时间)用 DATETIME + LocalDateTime,服务端统一用UTC存
* - 或者用 BIGINT 存毫秒时间戳,Java用 Instant
* - 避免用 TIMESTAMP(MySQL的TIMESTAMP会自动转换时区,有坑)
*/
// MyBatis-Plus / JPA 中的映射示例
// @Column(name = "created_at")
// private LocalDateTime createdAt; // 直接映射,无需转换
// @Column(name = "updated_timestamp")
// private Instant updatedAt; // 需要数据库驱动支持(MySQL 8.0+)
}
public static void main(String[] args) {
// 场景1:用户生日
UserProfile user = new UserProfile("张三",
LocalDate.of(1990, Month.AUGUST, 15));
System.out.println("年龄: " + user.getAge());
System.out.println("今天是生日: " + user.isBirthdayToday());
// 场景2:订单
Order order = new Order("ORDER001");
System.out.println("上海时区显示: " +
order.getCreatedAtForDisplay(ZoneId.of("Asia/Shanghai")));
System.out.println("纽约时区显示: " +
order.getCreatedAtForDisplay(ZoneId.of("America/New_York")));
// 场景3:营业时间
BusinessHours hours = new BusinessHours(
LocalTime.of(9, 0), LocalTime.of(18, 0));
System.out.println("现在营业中: " + hours.isOpenNow());
// 场景4:本地活动
LocalEvent event = new LocalEvent("技术分享",
LocalDateTime.of(2024, 1, 20, 14, 0),
LocalDateTime.of(2024, 1, 20, 16, 0));
System.out.println("活动时长: " + event.getDuration().toHours() + "小时");
}
}四、踩坑实录
坑1:Calendar月份从0开始,写出月份错位bug
报错现象:
Calendar cal = Calendar.getInstance();
cal.set(2024, 1, 15); // 以为是2024年1月15日
Date date = cal.getTime();
System.out.println(date); // Fri Feb 15 ... 2024 !是2月!根本原因:
Calendar的月份从0开始(0=January)。设置1,实际上是2月。这是Calendar最臭名昭著的设计。
具体解法:
用LocalDate,月份从1开始,符合人类直觉:
LocalDate date = LocalDate.of(2024, 1, 15); // 1月15日,正确
// 或者用Month枚举,更清晰
LocalDate date2 = LocalDate.of(2024, Month.JANUARY, 15);坑2:Date的getYear()返回奇怪的值
报错现象:
Date d = new Date();
System.out.println(d.getYear()); // 输出124,不是2024!根本原因:
Date.getYear()返回相对于1900年的偏移。2024 - 1900 = 124。这也是Date被废弃的原因之一。
具体解法:
int year = LocalDate.now().getYear(); // 2024,正确
// 或者
int year2 = Calendar.getInstance().get(Calendar.YEAR); // 2024(Calendar就这里还好用)坑3:LocalDateTime没有时区,跨时区比较出错
报错现象:
北京服务器和美国服务器的"相同本地时间",比较后结果不对。
根本原因:
// 北京服务器
LocalDateTime bj = LocalDateTime.of(2024, 1, 15, 10, 0); // 北京时间10:00
// 美国服务器
LocalDateTime us = LocalDateTime.of(2024, 1, 15, 10, 0); // 美国时间10:00
// 两个10:00其实相差16小时,但LocalDateTime不知道时区
System.out.println(bj.isBefore(us)); // false,但实际上北京10:00早很多具体解法:
// 需要跨时区比较,用Instant(UTC时间戳)
Instant bjInstant = LocalDateTime.of(2024, 1, 15, 10, 0)
.atZone(ZoneId.of("Asia/Shanghai")).toInstant();
Instant usInstant = LocalDateTime.of(2024, 1, 15, 10, 0)
.atZone(ZoneId.of("America/New_York")).toInstant();
System.out.println(bjInstant.isBefore(usInstant)); // true,北京10:00早于纽约10:00五、总结与延伸
三代API演进,一张表总结:
| 特性 | java.util.Date | java.util.Calendar | java.time(Java 8) |
|---|---|---|---|
| 线程安全 | 否(可变) | 否(可变) | 是(不可变) |
| 月份从0开始 | 是(坑) | 是(坑) | 否(1-12) |
| API设计 | 混乱 | 繁琐 | 流畅链式 |
| 时区支持 | 弱 | 一般 | 完善 |
| 推荐使用 | 不推荐 | 不推荐 | 推荐 |
迁移建议:
- 新项目:全部使用
java.time - 老项目:存在于接口层的Date参数逐步替换,工具类封装好互转方法
- MyBatis/JPA:配置好类型处理器,数据库DATETIME字段直接映射
LocalDateTime
如果你在Spring Boot项目里,Jackson的jackson-datatype-jsr310模块已经可以把LocalDateTime序列化/反序列化为JSON字符串,记得配置上:
spring:
jackson:
serialization:
write-dates-as-timestamps: false
date-format: yyyy-MM-dd HH:mm:ss
time-zone: Asia/Shanghai