| 類 | 包 | 精度 | 繼承 | 用途 |
| java.util.Date | java.util | 毫秒 | - | 通用日期時間,**不推薦新代碼使用** |
| java.sql.Date | java.sql | **天 (時分秒歸零)** | extends util.Date | 對應 SQL DATE (僅年月日) |
| java.sql.Time | java.sql | **毫秒 (日期歸零)** | extends util.Date | 對應 SQL TIME (僅時分秒) |
| java.sql.Timestamp | java.sql | **納秒 (getNanos())** | extends util.Date | 對應 SQL TIMESTAMP,**最高精度** |
| java.time.* | java.time (JDK 8+) | 納秒 | - | **現代首選**:LocalDateTime, Instant, ZonedDateTime... |
精度陷阱實戰演示
// 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+)
類型映射表
| SQL 類型 | JDBC 4.2+ get/set | java.time 類型 | 轉換 |
| DATE | getDate() / setDate() | LocalDate | rs.getObject(i, LocalDate.class) |
| TIME | getTime() / setTime() | LocalTime | rs.getObject(i, LocalTime.class) |
| TIMESTAMP | getTimestamp() / setTimestamp() | LocalDateTime | rs.getObject(i, LocalDateTime.class) |
| TIMESTAMP WITH TIME ZONE | getObject() | OffsetDateTime / ZonedDateTime | rs.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 時)
- 統一用 Timestamp 存儲完整精度,禁用
java.sql.Date/Time (精度截斷)。
- 比較時刻統一轉毫秒:
ts1.getTime() == ts2.getTime(),**忽略 nanos**,或自定義比較器 Comparator.comparingLong(Timestamp::getTime)。
- HashMap/HashSet 鍵禁用 Timestamp:用
Long (毫秒) 或 String (ISO 格式) 作鍵。
- 寫入前按目標數據庫精度截斷**:Oracle DATE →
ts.setNanos(0);MySQL DATETIME(3) → ts.setNanos((ts.getNanos()/1_000_000)*1_000_000)。
- 讀取後立即標準化:
Timestamp ts = rs.getTimestamp(i); if (ts != null) ts.setNanos(0); 統一精度再進入業務邏輯。
總結
Java 日期時間處理的核心矛盾:**JDBC 老類型精度不一、繼承體系破壞 equals 契約、數據庫精度異構**。解決路徑:
- 新項目全面拥抱 java.time (JDK 8+):不可變、線程安全、精度統一納秒、時區語義清晰、JDBC 4.2+ 原生支持。
- 遺留項目建立「精度標準化層」:DAO 層統一截斷/補齊 nanos,**業務層只見標準化後的時間對象**。
- 禁止 Timestamp 跳出持久層**:持久層轉換為
Long/String/LocalDateTime 再進入 Service 層。
- 測試覆蓋精度邊界**:毫秒邊界、納秒截斷、時區切換、夏令時變更、跨庫遷移。
時間是業務的骨架,**精度不一致就是骨折**。在架構層面統一時間模型,比在業務代碼到處加 setNanos(0) 要本質、徹底、可維護得多。
- 上一篇文章: JRockit Mission Control:生產級 Java 內存洩漏診斷與性能分析神器
- 下一篇文章: JRuby 整合 Rails 與 Java 生態:動態語言上 JVM 的架構機遇與挑戰
Copyright ©
程式碼之家 All Rights Reserved