從 JDBC 時代的 java.sql.Date 到 JSR-310 (java.time) 現代 API,Java 日期時間處理經歷了漫長演進。本文系統梳理舊版核心類別 (Date, Calendar, Timestamp, SimpleDateFormat) 的關係、轉換陷阱、格式化模式,並給出遷移至 java.time 的最佳實踐,解決「插入資料庫時間卻只有日期」、「字串解析拋異常」等經典痛點。
java.util.Date (毫秒精度、含日期時間、可變、非線程安全)
↑ extends
java.sql.Date (僅保留日期部分、toString() 格式 yyyy-MM-dd、JDBC DATE 對應)
↑ extends
java.sql.Timestamp (納秒精度、含日期時間、JDBC TIMESTAMP 對應)
關鍵:sql.Date 與 Timestamp 皆繼承 util.Date,但 sql.Date 強制將時分秒歸零 (標準化至 00:00:00),導致若直接用 new java.sql.Date(System.currentTimeMillis()) 插入資料庫,時間資訊會丟失。
long nowMs = System.currentTimeMillis(); // 毫秒時間戳
java.util.Date utilNow = new java.util.Date(); // 完整日期時間
java.sql.Date sqlDate = new java.sql.Date(nowMs); // 僅日期 (時分秒歸零)
java.sql.Timestamp ts = new Timestamp(nowMs); // 日期時間 + 納秒
| 來源 | 目標 | 代碼 | 注意 |
|---|---|---|---|
| util.Date | sql.Date | new java.sql.Date(utilDate.getTime()) | 時間部分會被截斷 |
| util.Date | Timestamp | new Timestamp(utilDate.getTime()) | 保留毫秒,納秒補 0 |
| sql.Date | Timestamp | new Timestamp(sqlDate.getTime()) | 時間為 00:00:00.0 |
| Timestamp | util.Date | new java.util.Date(ts.getTime()) | 納秒精度損失 |
| String | util.Date | sdf.parse(str) | 拋 ParseException,格式必須完全匹配 |
| String | sql.Date | new java.sql.Date(sdf.parse(str).getTime()) | 不可直接轉型 (java.sql.Date)sdf.parse() |
| util.Date | String | sdf.format(utilDate) | SimpleDateFormat 非線程安全 |
import java.sql.Timestamp;
import java.text.*;
import java.util.Date;
public class LegacyDateTimeDemo {
public static void main(String[] args) throws ParseException {
long now = System.currentTimeMillis(); // 1244711626453
Date utilDate = new Date(); // Thu Jun 11 16:27:57 CST 2009
java.sql.Date sqlDate = new java.sql.Date(now); // 2009-06-11
Timestamp ts = new Timestamp(now); // 2009-06-11 16:27:57.75
// 格式化
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
String formatted = sdf.format(ts); // 2009/06/11 16:27:57
// 解析
Date parsed = sdf.parse("2009/12/10 05:12:02"); // Thu Dec 10 05:12:02 CST 2009
java.sql.Date parsedSql = new java.sql.Date(parsed.getTime()); // 2009-12-10
Timestamp parsedTs = new Timestamp(parsedSql.getTime()); // 2009-12-10 05:12:02.0
System.out.println("utilDate: " + utilDate);
System.out.println("sqlDate: " + sqlDate);
System.out.println("timestamp: " + ts);
System.out.println("formatted: " + formatted);
System.out.println("parsedSql: " + parsedSql);
System.out.println("parsedTs: " + parsedTs);
}
}
| 符號 | 含義 | 範例 |
|---|---|---|
| y | 年 | 1996, 96 |
| M | 月 | July, Jul, 07 |
| d | 日 | 10 |
| H | 時 (0-23) | 0 |
| h | 時 (1-12) | 12 |
| m | 分 | 30 |
| s | 秒 | 55 |
| S | 毫秒 | 978 |
| E | 星期 | Tuesday, Tue |
| a | AM/PM | PM |
| z | 時區名稱 | Pacific Standard Time |
| Z | RFC 822 時區 | -0800 |
線程安全警告:SimpleDateFormat 非線程安全,多執行緒共用實例會導致解析錯誤或崩潰。解決方案:
ThreadLocal<SimpleDateFormat> 快取// 錯誤:stmt.setDate(1, new java.util.Date()) // 類型不匹配
// 錯誤:stmt.setDate(1, new java.sql.Date(new Date().getTime())) // 時間歸零!
// 正確:插入 TIMESTAMP (含時間)
PreparedStatement ps = conn.prepareStatement("INSERT INTO events(event_time) VALUES(?)");
ps.setTimestamp(1, new Timestamp(System.currentTimeMillis()));
ps.executeUpdate();
// 正確:插入 DATE (僅日期)
ps.setDate(1, new java.sql.Date(System.currentTimeMillis()));
Oracle 特例:DATE 型別含時間,若用 setDate 會截斷時間,必須用 setTimestamp 或 oracle.sql.DATE。
舊版 API 設計缺陷:可變、非線程安全、月份 0-based、無時區概念、API 不一致。java.time 全面修正:
| 舊版 | 新版 (java.time) | 特性 |
|---|---|---|
| java.util.Date | Instant (時間戳)、LocalDateTime (本地日期時間) | 不可變、納秒精度 |
| java.sql.Date | LocalDate | 僅日期 |
| java.sql.Timestamp | LocalDateTime / OffsetDateTime | 含時區偏移 |
| Calendar | ZonedDateTime | 完整時區規則 |
| SimpleDateFormat | DateTimeFormatter | 不可變、線程安全、預定義常量 |
// 當前時間
LocalDateTime now = LocalDateTime.now(); // 2024-01-15T14:30:45.123
Instant instant = Instant.now(); // 2024-01-15T06:30:45.123Z (UTC)
// 格式化 (線程安全)
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
String str = now.format(fmt); // 2024/01/15 14:30:45
// 解析
LocalDateTime parsed = LocalDateTime.parse(str, fmt);
// 與舊版互轉 (JDBC 4.2+ 驅動支援直接 setObject/getObject)
java.sql.Timestamp legacyTs = Timestamp.valueOf(now); // LocalDateTime → Timestamp
LocalDateTime modern = legacyTs.toLocalDateTime(); // Timestamp → LocalDateTime
// 資料庫操作 (JDBC 4.2+)
ps.setObject(1, now); // 直接寫入 TIMESTAMP
LocalDateTime dbTime = rs.getObject(1, LocalDateTime.class); // 直接讀取
java.time,拒絕 java.util.Date/Calendar。LocalDateTime 等。setObject/getObject 直接綁定 java.time 類型)。JavaTimeModule),日期格式統一 ISO-8601。ZoneId.systemDefault(),業務邏輯顯式指定 ZoneId.of("Asia/Taipei")。理解 java.util.Date、java.sql.Date、Timestamp 繼承關係與 SimpleDateFormat 線程不安全本質,是修復舊系統時間 Bug 的鑰匙。但長遠來看,全面遷移至 java.time 才是根治之道:不可變物件消除併發隱患、清晰的類別職責消除語義混淆、標準化格式化/解析 API 提升開發體驗。JDK 8 已發布十年,java.time 早已生產就緒,請立即啟動遷移計畫。