首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 程式設計入門

Java 日期時間處理避坑指南:Date、Time、Timestamp 精度陷阱與最佳實踐

     編輯:Java 程式設計入門

核心痛點:數據庫精度不一致導致業務邏輯失效

災難場景:Java 端 Timestamp 含毫秒/納秒 → 寫入 Oracle DATE 截斷 → 讀出比較 equals() 失敗 → 業務判斷錯誤 (如幂等檢查、樂觀鎖版本號)。

JDBC 時間類型層級對比

數據庫時間類型精度備註
MySQL 5.6+DATETIME / TIMESTAMP微秒 (6 位)默認 0 位,需顯式 DATETIME(6)
OracleDATE秒**無毫秒**;TIMESTAMP 類型支持納秒
PostgreSQLTIMESTAMP微秒
SQL ServerDATETIME2100 納秒舊 DATETIME 只有 3.33ms 精度

精度陷阱實戰演示

// 1. 數據庫存入 Timestamp(2023-10-01 12:30:45.123456789)
// 2. Oracle DATE 列截斷為 2023-10-01 12:30:45
// 3. 讀出 java.sql.Timestamp ts = rs.getTimestamp("col");
//    ts.getTime()     → 1696137045000 (毫秒,截斷後)
//    ts.getNanos()    → 0 (數據庫無納秒)
// 4. 業務代碼比較
Timestamp original = Timestamp.valueOf("2023-10-01 12:30:45.123456789");
Timestamp fromDb   = rs.getTimestamp("col");
original.equals(fromDb); // FALSE! 因為 nanos 不等 (123456789 vs 0)

// 更隱蔽:util.Date 比較
Date d1 = new Date(original.getTime()); // 只有毫秒
Date d2 = new Date(fromDb.getTime());   // 只有毫秒
d1.equals(d2); // TRUE (毫秒相同) → **錯誤的安全感**

equals() 非對稱性陷阱

// Timestamp.equals(Object) 源碼邏輯:
public boolean equals(Object obj) {
    if (!(obj instanceof Timestamp)) return false; // 關鍵:非 Timestamp 直接 false
    Timestamp other = (Timestamp) obj;
    return super.equals(other) && this.nanos == other.nanos; // 比較毫秒 + 納秒
}

// 後果:
Timestamp ts = new Timestamp(1000); // nanos=0
Date d = new Date(1000);
ts.equals(d); // FALSE (d 不是 Timestamp)
d.equals(ts); // TRUE (Date.equals 比較 getTime() 毫秒)
// **破壞 equals 對稱性契約**,導致 HashMap/HashSet 異常行為

最佳實踐:遷移至 java.time (JDK 8+)

類型映射表

類包精度繼承用途
java.util.Datejava.util毫秒-通用日期時間,**不推薦新代碼使用**
java.sql.Datejava.sql**天 (時分秒歸零)**extends util.Date對應 SQL DATE (僅年月日)
java.sql.Timejava.sql**毫秒 (日期歸零)**extends util.Date對應 SQL TIME (僅時分秒)
java.sql.Timestampjava.sql**納秒 (getNanos())**extends util.Date對應 SQL TIMESTAMP,**最高精度**
java.time.*java.time (JDK 8+)納秒-**現代首選**:LocalDateTime, Instant, ZonedDateTime...
SQL 類型JDBC 4.2+ get/setjava.time 類型轉換
DATEgetDate() / setDate()LocalDaters.getObject(i, LocalDate.class)
TIMEgetTime() / setTime()LocalTimers.getObject(i, LocalTime.class)
TIMESTAMPgetTimestamp() / setTimestamp()LocalDateTimers.getObject(i, LocalDateTime.class)
TIMESTAMP WITH TIME ZONEgetObject()OffsetDateTime / ZonedDateTimers.getObject(i, OffsetDateTime.class)

JPA/Hibernate 映射註解

@Entity
public class Event {
    @Column(columnDefinition = "TIMESTAMP(6)") // MySQL 微秒
    private LocalDateTime createdAt; // 推薦:無時區業務時間

    @Column(columnDefinition = "TIMESTAMPTZ") // PostgreSQL/Oracle 帶時區
    private OffsetDateTime updatedAt; // 推薦:分布式系統全局時間

    // 自動填充
    @PrePersist
    void onCreate() { this.createdAt = LocalDateTime.now(); }
    @PreUpdate
    void onUpdate() { this.updatedAt = OffsetDateTime.now(ZoneOffset.UTC); }
}

MyBatis TypeHandler 自定義 (兼容舊驅動)

@MappedTypes(LocalDateTime.class)
@MappedJdbcTypes(JdbcType.TIMESTAMP)
public class LocalDateTimeTypeHandler extends BaseTypeHandler {
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, LocalDateTime param, JdbcType jdbcType) throws SQLException {
        ps.setTimestamp(i, Timestamp.valueOf(param));
    }
    @Override
    public LocalDateTime getNullableResult(ResultSet rs, String columnName) throws SQLException {
        Timestamp ts = rs.getTimestamp(columnName);
        return ts != null ? ts.toLocalDateTime() : null;
    }
    // getNullableResult(ResultSet, int) / getNullableResult(CallableStatement, int) 同理
}

遺留代碼修復清單 (無法立即遷移 java.time 時)

  1. 統一用 Timestamp 存儲完整精度,禁用 java.sql.Date/Time (精度截斷)。
  2. 比較時刻統一轉毫秒:ts1.getTime() == ts2.getTime(),**忽略 nanos**,或自定義比較器 Comparator.comparingLong(Timestamp::getTime)。
  3. HashMap/HashSet 鍵禁用 Timestamp:用 Long (毫秒) 或 String (ISO 格式) 作鍵。
  4. 寫入前按目標數據庫精度截斷**:Oracle DATE → ts.setNanos(0);MySQL DATETIME(3) → ts.setNanos((ts.getNanos()/1_000_000)*1_000_000)。
  5. 讀取後立即標準化:Timestamp ts = rs.getTimestamp(i); if (ts != null) ts.setNanos(0); 統一精度再進入業務邏輯。

總結

Java 日期時間處理的核心矛盾:**JDBC 老類型精度不一、繼承體系破壞 equals 契約、數據庫精度異構**。解決路徑:

  1. 新項目全面拥抱 java.time (JDK 8+):不可變、線程安全、精度統一納秒、時區語義清晰、JDBC 4.2+ 原生支持。
  2. 遺留項目建立「精度標準化層」:DAO 層統一截斷/補齊 nanos,**業務層只見標準化後的時間對象**。
  3. 禁止 Timestamp 跳出持久層**:持久層轉換為 Long/String/LocalDateTime 再進入 Service 層。
  4. 測試覆蓋精度邊界**:毫秒邊界、納秒截斷、時區切換、夏令時變更、跨庫遷移。

時間是業務的骨架,**精度不一致就是骨折**。在架構層面統一時間模型,比在業務代碼到處加 setNanos(0) 要本質、徹底、可維護得多。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved