从混乱到清晰,从线程不安全到不可变,彻底搞懂 java 时间处理的进化之路
一、先上全家福(一张表看懂所有类)
| 类 | 所属包 | 包含内容 | 是否可变 | 线程安全 | 核心用途 |
|---|---|---|---|---|---|
| date | java.util | 日期 + 时间(毫秒精度) | ✅ 可变 | ❌ 不安全 | 老项目遗留,时间戳表示 |
| localdate | java.time | 仅日期(年-月-日) | ❌ 不可变 | ✅ 安全 | 生日、订单日期、报表日期 |
| localtime | java.time | 仅时间(时:分:秒.纳秒) | ❌ 不可变 | ✅ 安全 | 营业时间、定时任务时间 |
| localdatetime | java.time | 日期 + 时间(无时区) | ❌ 不可变 | ✅ 安全 | 数据库 datetime 字段、本地时间展示 |
| zoneddatetime | java.time | 日期 + 时间 + 时区 | ❌ 不可变 | ✅ 安全 | 跨时区系统、全球化应用 |
| instant | java.time | utc 时间戳 | ❌ 不可变 | ✅ 安全 | 机器时间、日志时间戳 |
| simpledateformat | java.text | 格式化/解析 date | 可变(内部 calendar) | ❌ 线程不安全 | 老项目格式化 |
| datetimeformatter | java.time.format | 格式化/解析 java.time 类 | 不可变 | ✅ 安全 | 新项目格式化(推荐) |
黄金选型原则:
- 新项目:一律使用
java.time包(localdate、localdatetime、datetimeformatter) - 遗留项目:能迁就迁,不能迁也要正确使用
simpledateformat(threadlocal)
二、类关系图解(一目了然)
┌─────────────────────────────────────────────────────────────────────┐
│ java 日期时间类体系 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 📦 java.util(老 api,遗留) │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ date │ 日期+时间(毫秒),可变,设计糟糕 │ │
│ │ calendar │ 日期运算,混乱,几乎废弃 │ │
│ │ simpledateformat │ 格式化/解析 date,线程不安全!! │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ 📦 java.time(新 api,java 8+,推荐) │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 日期类 │ 时间类 │ 带时区 │ │
│ │ localdate (年月日) │ localtime (时分秒) │ zoneddatetime│ │
│ │ └──────┬──────────────┴──────┬────────────┘ │ │
│ │ │ 组合 │ │ │
│ │ └───── localdatetime ─┘ │ │
│ │ │ │ │
│ │ ┌──────────┼──────────┐ │ │
│ │ ↓ ↓ ↓ │ │
│ │ 格式化/解析 时间戳 日期运算 │ │
│ │ datetimeformatter instant period/duration │ │
│ │ (线程安全!) (utc时间) (间隔计算) │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
三、date:老旧的“万能军刀”(该退休了)
1. 创建与基础使用
import java.util.date; // 1. 获取当前时间 date now = new date(); system.out.println(now); // tue aug 18 14:30:25 cst 2026 // 2. 通过时间戳创建(毫秒) date datefrommillis = new date(1723967425000l); // 3. 获取/设置时间戳(可变!) long timestamp = now.gettime(); // 毫秒级时间戳 now.settime(1723967425000l); // ❌ 可变,破坏了不可变性
2. 令人抓狂的设计缺陷
// ❌ 年份从 1900 开始计算,月份从 0 开始 date d = new date(2026 - 1900, 8 - 1, 18); // 8月要写成 7! system.out.println(d.getyear()); // 126(实际年份要 +1900) system.out.println(d.getmonth()); // 7(实际月份要 +1) // ❌ 方法大量被废弃 d.getyear(); // @deprecated d.getmonth(); // @deprecated d.getday(); // @deprecated
结论:date 已被官方标记为“legacy”,新项目绝对不要用。
四、localdate:仅日期(最常用的日期类)
1. 创建与基础使用
import java.time.localdate;
// ✅ 获取当前日期
localdate today = localdate.now(); // 2026-08-18
// ✅ 创建指定日期(符合直觉:年-月-日)
localdate date = localdate.of(2026, 8, 18);
localdate date2 = localdate.parse("2026-08-18"); // iso 标准格式
// ✅ 获取年月日
int year = date.getyear(); // 2026
int month = date.getmonthvalue(); // 8(注意是 getmonthvalue,不是 getmonth)
int day = date.getdayofmonth(); // 18
int dayofweek = date.getdayofweek().getvalue(); // 3(周三=3,周一=1)
2. 日期运算(不可变,返回新对象)
localdate today = localdate.now();
localdate tomorrow = today.plusdays(1); // 加 1 天
localdate nextmonth = today.plusmonths(1); // 加 1 月
localdate nextyear = today.plusyears(1); // 加 1 年
localdate lastweek = today.minusweeks(1); // 减 1 周
localdate plusday = today.plusdays(100); // 加 100 天
// 判断
boolean isafter = today.isafter(localdate.of(2026, 1, 1)); // true
boolean isleap = today.isleapyear(); // 判断闰年
// 链式调用
localdate future = today
.plusyears(2)
.minusmonths(3)
.plusdays(10); // 2028-05-28
3. 常见业务场景
// 统计当月天数 int daysinmonth = yearmonth.now().lengthofmonth(); // 31 // 判断是否为闰年 boolean isleap = localdate.now().isleapyear(); // false(2026 不是闰年) // 获取某个月的第一天/最后一天 localdate firstday = localdate.of(2026, 8, 1); localdate lastday = firstday.with(temporaladjusters.lastdayofmonth()); // 2026-08-31
五、localdatetime:日期 + 时间(最常用的组合类)
1. 创建与基础使用
import java.time.localdatetime;
import java.time.localdate;
import java.time.localtime;
// ✅ 获取当前日期时间
localdatetime now = localdatetime.now(); // 2026-08-18t14:30:25.123456789
// ✅ 从年月日时分秒创建
localdatetime dt = localdatetime.of(2026, 8, 18, 14, 30, 25);
// ✅ 由 localdate + localtime 组合
localdate date = localdate.of(2026, 8, 18);
localtime time = localtime.of(14, 30, 25);
localdatetime dt2 = localdatetime.of(date, time);
// ✅ 从字符串解析(iso 标准:t 分隔)
localdatetime parsed = localdatetime.parse("2026-08-18t14:30:25");
// ✅ 获取各字段
int year = now.getyear();
int month = now.getmonthvalue();
int day = now.getdayofmonth();
int hour = now.gethour();
int minute = now.getminute();
int second = now.getsecond();
int nano = now.getnano();
2. 日期时间运算
localdatetime now = localdatetime.now();
localdatetime later = now
.plusyears(1)
.plusmonths(2)
.plusdays(3)
.plushours(4)
.plusminutes(5)
.plusseconds(6); // 链式调用,灵活直观
// 比较
boolean before = now.isbefore(localdatetime.of(2027, 1, 1, 0, 0)); // true
boolean after = now.isafter(localdatetime.now().minusdays(1)); // true
六、localtime:仅时间
import java.time.localtime;
// ✅ 创建
localtime now = localtime.now(); // 14:30:25.123456789
localtime time = localtime.of(14, 30, 25); // 14:30:25
localtime parsed = localtime.parse("14:30:25");
// ✅ 运算
localtime later = time.plushours(1).plusminutes(30).plusseconds(10);
// ✅ 获取字段
int hour = time.gethour(); // 14
int minute = time.getminute(); // 30
int second = time.getsecond(); // 25
int nano = time.getnano();
七、格式化:simpledateformatvsdatetimeformatter
1.simpledateformat(老 api,线程不安全)
import java.text.simpledateformat;
import java.util.date;
// ❌ 危险:不能作为 static 共享
simpledateformat sdf = new simpledateformat("yyyy-mm-dd hh:mm:ss");
// 格式化:date → string
string str = sdf.format(new date()); // "2026-08-18 14:30:25"
// 解析:string → date
date date = sdf.parse("2026-08-18 14:30:25"); // 需处理 parseexception
线程不安全原理:
simpledateformat 内部维护了 calendar 对象。多线程同时 format/parse 时会并发修改这个共享 calendar,导致:
- 日期错乱(2026-08-18 变成 2025-07-17)
numberformatexception- 线上偶发故障,排查极难
正确用法:
// 方案1:每次新建(低频场景)
public string format(date date) {
return new simpledateformat("yyyy-mm-dd").format(date);
}
// 方案2:threadlocal(推荐,性能最佳)
public class dateutils {
private static final threadlocal<simpledateformat> tl =
threadlocal.withinitial(() -> new simpledateformat("yyyy-mm-dd"));
public static string format(date date) {
return tl.get().format(date);
}
}
// 方案3:同步锁(不推荐,性能差)
synchronized (sdf) {
sdf.format(date);
}
2.datetimeformatter(新 api,线程安全,推荐)
import java.time.localdatetime;
import java.time.format.datetimeformatter;
// ✅ 不可变,可定义为全局静态常量!
public static final datetimeformatter formatter =
datetimeformatter.ofpattern("yyyy-mm-dd hh:mm:ss");
// 格式化:localdatetime → string
localdatetime now = localdatetime.now();
string str = now.format(formatter); // "2026-08-18 14:30:25"
// 解析:string → localdatetime
localdatetime dt = localdatetime.parse("2026-08-18 14:30:25", formatter);
// ✅ 使用预定义格式
string iso1 = localdatetime.now().format(datetimeformatter.iso_local_date_time);
string iso2 = localdate.now().format(datetimeformatter.iso_local_date);
// ✅ 自定义格式(中文)
datetimeformatter chineseformatter =
datetimeformatter.ofpattern("yyyy年mm月dd日 hh时mm分ss秒");
string str = localdatetime.now().format(chineseformatter); // 2026年08月18日 14时30分25秒
3. 格式化模式对照表(强烈收藏)
| 字母 | 含义 | 示例 |
|---|---|---|
yyyy | 年份 | 2026 |
yy | 年份(后两位) | 26 |
mm | 月份 | 08 |
m | 月份(不带前导零) | 8 |
dd | 日 | 18 |
d | 日(不带前导零) | 18 |
hh | 小时(24 小时制) | 14 |
hh | 小时(12 小时制) | 02 |
mm | 分钟 | 30 |
ss | 秒 | 25 |
sss | 毫秒 | 123 |
ssssss | 纳秒 | 123456 |
a | 上午/下午(am/pm) | pm |
e | 星期(简写) | wed |
eee | 星期(简写) | 周三 |
eeee | 星期(全称) | 星期三 |
z | 时区 | cst |
z | 时区(偏移量) | +0800 |
g | 公元 | 公元(ad) |
// 常用格式示例
datetimeformatter.ofpattern("yyyy-mm-dd hh:mm:ss") // 2026-08-18 14:30:25
datetimeformatter.ofpattern("yyyy年mm月dd日") // 2026年08月18日
datetimeformatter.ofpattern("yyyy/mm/dd hh:mm") // 2026/08/18 14:30
datetimeformatter.ofpattern("yyyy-mm-dd't'hh:mm:ss") // 2026-08-18t14:30:25
datetimeformatter.ofpattern("yyyy-mm-dd hh:mm:ss.sss") // 2026-08-18 14:30:25.123
datetimeformatter.ofpattern("eee, mmm dd, yyyy") // wed, aug 18, 2026
八、新旧 api 互转(迁移必看)
import java.time.*;
import java.util.date;
public class dateconverter {
// ===== date ↔ localdate =====
public static localdate datetolocaldate(date date) {
return date.toinstant()
.atzone(zoneid.systemdefault())
.tolocaldate();
}
public static date localdatetodate(localdate localdate) {
return date.from(localdate.atstartofday(zoneid.systemdefault()).toinstant());
}
// ===== date ↔ localdatetime =====
public static localdatetime datetolocaldatetime(date date) {
return date.toinstant()
.atzone(zoneid.systemdefault())
.tolocaldatetime();
}
public static date localdatetimetodate(localdatetime ldt) {
return date.from(ldt.atzone(zoneid.systemdefault()).toinstant());
}
// ===== date ↔ instant =====
public static instant datetoinstant(date date) {
return date.toinstant();
}
public static date instanttodate(instant instant) {
return date.from(instant);
}
// ===== localdatetime ↔ localdate =====
public static localdate localdatetimetolocaldate(localdatetime ldt) {
return ldt.tolocaldate();
}
public static localdatetime localdatetolocaldatetime(localdate ld) {
return ld.atstartofday(); // 00:00:00
}
}
九、实战避坑总结(高频错题)
| 常见错误 | 后果 | 正确做法 |
|---|---|---|
simpledateformat 定义为 static 共享 | 多线程日期错乱、numberformatexception | 使用 threadlocal 或换 datetimeformatter |
date 的月份从 0 开始 | 日期显示为 8 月却输出 7 月 | 新项目用 localdate/localdatetime |
date 用 getmonth() 判断 | 逻辑错误 | 用 localdate.getmonthvalue() |
用 new date(2026, 7, 18) | 年变为 3926 年,月份偏移 | 用 localdate.of(2026, 8, 18) |
用 localdatetime 跨时区 | 时间展示不一致 | 跨时区用 zoneddatetime |
字符串解析不处理 parseexception | 程序异常崩溃 | try-catch 或使用 datetimeformatter(不抛受检异常) |
datetimeformatter.ofpattern("yyyy-mm-dd") 传入 localdatetime | 编译通过但运行正常 | 虽然可以,但建议用对应类型:localdate 格式用 localdate |
十、思考题(检验是否真的懂了)
// 问题 1:下面这段代码在高并发下会出现什么问题?
public class dateutils {
public static final simpledateformat sdf =
new simpledateformat("yyyy-mm-dd hh:mm:ss");
public static string format(date date) {
return sdf.format(date);
}
}
// 问题 2:下面代码分别输出什么?
localdate date = localdate.of(2026, 8, 18);
system.out.println(date.getmonthvalue()); // a
system.out.println(date.getmonth()); // b
date d = new date(126, 7, 18);
system.out.println(d.getmonth() + 1); // c
// 问题 3:下面两种方式,哪一种性能更好、更安全?
// 方式 a
datetimeformatter formatter = datetimeformatter.ofpattern("yyyy-mm-dd");
string str = localdate.now().format(formatter);
// 方式 b
string str = localdate.now().format(datetimeformatter.ofpattern("yyyy-mm-dd"));
答案(选中下方空白区域查看):
- 多线程并发调用时会出现日期解析错误、
numberformatexception等随机异常,因为simpledateformat线程不安全。 - a 输出
8(getmonthvalue()返回 1-12);b 输出august(getmonth()返回枚举month);c 输出8(date月份从 0 开始,7+1=8)。 - 方式 a 更好:复用同一个
formatter对象(线程安全),方式 b 每次调用都创建新对象,有性能开销。
总结(终极速查表)
| 场景 | 推荐类 | 理由 |
|---|---|---|
| 只需要日期(年月日) | localdate | 轻量、不可变、线程安全 |
| 只需要时间(时分秒) | localtime | 轻量、不可变、线程安全 |
| 日期 + 时间(本地) | localdatetime | 最常用,对应数据库 datetime |
| 日期 + 时间 + 时区 | zoneddatetime | 全球化应用 |
| 时间戳(机器时间) | instant | utc 时间,适合日志、排序 |
| 格式化(新项目) | datetimeformatter | 不可变、线程安全、功能强大 |
| 格式化(遗留项目) | simpledateformat | 必须用 threadlocal 或每次新建 |
| 日期时间间隔 | period(年月日)/ duration(时分秒) | 计算差值 |
到此这篇关于java日期时间api date、localdate、localdatetime、simpledateformat、datetimeformatter全攻略的文章就介绍到这了,更多相关java日期时间api内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论