时间戳(epoch timestamp)转日期看似简单,但跨系统、跨语言、跨时区时坑多到能让你头大一圈。"10 位还是 13 位"、"秒还是毫秒"、"UTC 还是本地时间"、"Instant 为什么不能加时区"、"LocalDateTime 怎么带时区输出"——每个坑都踩过不止一次。
这篇文章把 Java 时间转换的 5 个高频坑按「数字判断 → 时区 → 新旧 API 互转 → 数据库存储」的顺序理一遍。
坑 1:10 位 vs 13 位 → 秒 vs 毫秒
long ts1 = 1761800000L; // 10 位,秒
long ts2 = 1761800000000L; // 13 位,毫秒
long ts3 = 1761800000000000L; // 16 位,微秒(少见)
判断规则:按数量级判断最可靠——绝对值小于 1e12 视为秒(10 位),大于等于 1e12 视为毫秒(13 位),16 位则是微秒。10 位是秒、13 位是毫秒只是经验说法,直接和临界值比较更稳。Java System.currentTimeMillis() 输出毫秒,但很多服务端 API 返回秒(比如 GitLab、GitHub 的某些接口)。
// 安全判断
Instant instant;
if (Math.abs(ts) >= 1e12) {
instant = Instant.ofEpochMilli(ts);
} else {
instant = Instant.ofEpochSecond(ts);
}
用在线工具验证:时间戳转换工具 自动识别位数、同时输出秒和毫秒两种时间,一眼就知道自己手里的数字是哪种。
坑 2:时区
Java 8 之后时间 API 的时区层级:
Instant ← 无时区(就是 UTC 秒数)
│
├── ZonedDateTime ← 带时区的完整时间
├── LocalDateTime ← 无时区("2026-10-29T10:00:00")
└── OffsetDateTime ← 带偏移量的时间(+08:00)
Instant 和 LocalDateTime 的关系:Instant 加一个时区偏移就能得到 LocalDateTime。同一个 Instant 在不同时区下 LocalDateTime 不一样。
Instant instant = Instant.ofEpochSecond(1761800000L);
// Instant + ZoneId = LocalDateTime
LocalDateTime beijingTime = LocalDateTime.ofInstant(instant, ZoneId.of("Asia/Shanghai")); // 2025-10-30T12:53:20
LocalDateTime utcTime = LocalDateTime.ofInstant(instant, ZoneOffset.UTC); // 2025-10-30T04:53:20
坑点:LocalDateTime.now() 取的是系统默认时区(服务器在中国就是 UTC+8),Instant.now() 永远是 UTC——同一个 now(),值不一样。数据库存储一律用 Instant + UTC,展示时再加用户时区偏移。
坑 3:旧 API 互转
Java 8 之前用 java.util.Date、java.sql.Timestamp、java.sql.Time、java.sql.Date,Java 8+ 用 java.time。互相转换必须手动 bridge:
// Date ↔ Instant
Date.from(instant); // Instant → Date
date.toInstant(); // Date → Instant
// Timestamp ↔ LocalDateTime
Timestamp.valueOf(localDateTime); // LocalDateTime → Timestamp
timestamp.toLocalDateTime(); // Timestamp → LocalDateTime
// java.sql.Date 专门给 JDBC,只保留年月日
java.sql.Date.valueOf(localDate); // LocalDate → java.sql.Date
坑点:new Date(long) 用的是毫秒(和 System.currentTimeMillis() 对不上秒级 timestamp 会出错);Timestamp 有纳秒精度(getNanos()),转 Instant 时纳秒会保留下来。
坑 4:格式化输出
// ✅ 正确:DateTimeFormatter 是不可变的,线程安全
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
.withZone(ZoneId.of("Asia/Shanghai"));
String text = fmt.format(instant);
// ❌ 错误:SimpleDateFormat 不是线程安全的,不能当 static 共享
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
坑点:如果要输出 UTC 时间(给前端 JS / 给数据库),时区要显式指定,不要让 DateTimeFormatter 默认取系统时区。
坑 5:数据库存储的时区
MySQL 的 DATETIME 存的是无时区时间——写入 "2026-10-31 10:00:00" 读出来还是这个字符串,不做时区转换。但 TIMESTAMP 存的是 UTC,写入时按 session 时区转 UTC、读出时再转回 session 时区。
// MyBatis + MySQL:推荐
// Java 侧用 LocalDateTime(无时区),对应 DATETIME
// Java 侧用 Instant / OffsetDateTime,对应 TIMESTAMP
如果用 JPA 的 @Temporal(TemporalType.TIMESTAMP) + MySQL DATETIME,java.util.Date / Timestamp 虽然内部是 UTC 毫秒点,但 JDBC 写库和读库都按 JVM 默认时区转成字面量——服务器时区一变(比如从东八区改成 UTC),同一列读出的时刻就整体偏移。用 LocalDateTime 映射 DATETIME 不会有这个问题,因为整个链路没有时区参与转换。
用在线工具快速验证
时间戳转换工具 的核心功能:
- 自动识别位数:10 位秒 / 13 位毫秒自动判断,并把两种解读同时列出,不确定单位时一眼看出哪个对
- 时区切换:UTC / 北京时间 / 东京时间 / 印度时间 / 伦敦 / 纽约一键切换,同一时间戳在不同时区的显示结果直接对比
- 当前时间实时生成:秒级和毫秒级同时显示,方便直接粘进代码
常见坑
- "服务器是 UTC,本地显示怎么是别的时间":前端 JS 的
new Date(ts * 1000)自动按浏览器时区转,不需要手动加减偏移。注意一个例外:后端若返回不带时区标识的字符串(如"2026-10-29 12:00:00"),浏览器会按本地时间解析,这时才有歧义——接口尽量返回时间戳或带Z的 ISO 8601。 - "MySQL DATETIME 存的是 UTC+8,读出来却变成 UTC":这个前提本身站不住——
DATETIME不做任何时区转换,存什么读什么。出现这种"漂移"通常是三个原因之一:列其实是TIMESTAMP(按连接time_zone转换)、JDBC URL 的serverTimezone与应用时区不一致、或写入前应用层已转过一次造成双重转换。先SHOW CREATE TABLE确认列类型,再查连接时区。 - "Date 比 LocalDateTime 老 8 小时":
new Date()内部存 UTC,但toString()自动按本地时区显示。两个 API 不要混用。 - "LocalDateTime.now() 和 System.currentTimeMillis() 对不上":前者用系统时区后者输出毫秒,除了除以 1000 还要对齐时区。
相关工具
- 时间戳在线转换:秒/毫秒自动识别、多时区对比、当前时间一键生成