Java 日期時間 API 經歷了三代演進:java.util.Date (1.0) → Calendar (1.1) → java.time (Java 8+)。本文系統梳理三代 API 用法、線程安全問題、格式化解析、時區處理、期間計算、最佳實踐遷移指南。
| 特性 | Date (1.0) | Calendar (1.1) | java.time (8+) |
|---|---|---|---|
| 線程安全 | ❌ 可變 | ❌ 可變 | ✅ 不可變 |
| API 設計 | 混亂、缺陷多 | 複雜、易錯 | 清晰、領域驅動 |
| 時區支持 | 弱 | 一般 | 強 (ZoneId/ZoneOffset) |
| 期間/持續時間 | 無 | 無 | Period/Duration |
| 格式化/解析 | SimpleDateFormat (非線程安全) | 同左 | DateTimeFormatter (線程安全) |
| 推薦程度 | ❌ 棄用 | ⚠️ 維舊 | ✅ 首選 |
Instant 時間線上的瞬間 (UTC,納秒精度)
│
├── LocalDate 本地日期 (年月日,無時區)
├── LocalTime 本地時間 (時分秒納秒,無時區)
├── LocalDateTime 本地日期時間 (無時區)
│
├── ZonedDateTime 帶時區日期時間 (最完整)
├── OffsetDateTime 帶偏移量日期時間
├── OffsetTime 帶偏移量時間
│
├── Duration 基於時間的量 (秒/納秒,適合 Instant)
├── Period 基於日期的量 (年月日,適合 LocalDate)
│
├── ZoneId 時區 ID (如 Asia/Shanghai)
├── ZoneOffset 時區偏移 (如 +08:00)
│
├── DateTimeFormatter 格式化/解析 (線程安全)
├── TemporalAdjusters 日期調整器 (下一個周一、月末等)
└── Clock 時鐘 (可注入,便於測試)
// 獲取當前瞬間
Instant now = Instant.now(); // UTC
Instant.now(Clock.systemUTC()); // 可注入時鐘
// 時間戳互轉
long epochMilli = now.toEpochMilli(); // 毫秒
long epochSecond = now.getEpochSecond(); // 秒
Instant instant = Instant.ofEpochMilli(epochMilli);
Instant instant2 = Instant.ofEpochSecond(epochSecond, nanoAdjustment);
// 運算
Instant later = now.plusSeconds(3600).plusMillis(500);
Instant earlier = now.minus(Duration.ofDays(1));
// 比較
boolean before = now.isBefore(later);
// 創建
LocalDate date = LocalDate.now(); // 系統默認時區
LocalDate date2 = LocalDate.of(2024, 3, 15);
LocalDate date3 = LocalDate.parse("2024-03-15");
LocalTime time = LocalTime.now();
LocalTime time2 = LocalTime.of(14, 30, 0);
LocalTime time3 = LocalTime.parse("14:30:00");
LocalDateTime dt = LocalDateTime.now();
LocalDateTime dt2 = LocalDateTime.of(date2, time2);
LocalDateTime dt3 = LocalDateTime.parse("2024-03-15T14:30:00");
// 獲取字段
int year = dt.getYear();
Month month = dt.getMonth();
int day = dt.getDayOfMonth();
DayOfWeek dow = dt.getDayOfWeek();
int hour = dt.getHour();
// 運算(返回新對象,不可變)
LocalDateTime nextWeek = dt.plusWeeks(1);
LocalDate firstDayOfMonth = dt.withDayOfMonth(1);
LocalDate nextMonday = dt.with(TemporalAdjusters.next(DayOfWeek.MONDAY));
LocalDate lastDayOfYear = dt.with(TemporalAdjusters.lastDayOfYear());
// 截取
LocalDate datePart = dt.toLocalDate();
LocalTime timePart = dt.toLocalTime();
// 創建
ZoneId shanghai = ZoneId.of("Asia/Shanghai");
ZonedDateTime zdt = ZonedDateTime.now(shanghai);
ZonedDateTime zdt2 = LocalDateTime.of(2024, 3, 15, 14, 30)
.atZone(shanghai);
// 時區轉換(瞬間不變,顯示時間變)
ZonedDateTime utc = zdt.withZoneSameInstant(ZoneOffset.UTC);
ZonedDateTime ny = zdt.withZoneSameInstant(ZoneId.of("America/New_York"));
// 保持本地時間不變,改變時區(瞬間改變)
ZonedDateTime sameLocal = zdt.withZoneSameLocal(ZoneId.of("America/New_York"));
// 夏令時處理自動
// 2024-03-10 02:00 美國夏令時開始,時鐘撥快 1 小時
ZonedDateTime dst = ZonedDateTime.of(2024, 3, 10, 1, 30, 0, 0,
ZoneId.of("America/New_York")); // 實際為 EST
ZonedDateTime afterDst = dst.plusHours(2); // 自動變為 EDT,顯示 03:30
// Duration: 時間量,精確到納秒
Duration d1 = Duration.between(Instant.now(), Instant.now().plusSeconds(3600));
Duration d2 = Duration.ofHours(2).plusMinutes(30);
long seconds = d1.getSeconds();
long millis = d1.toMillis();
long days = d1.toDays(); // 24小時制天數
// Period: 日期量,年月日
Period p1 = Period.between(LocalDate.of(2020, 1, 1), LocalDate.of(2024, 3, 15));
Period p2 = Period.ofYears(2).plusMonths(3);
int years = p1.getYears();
int months = p1.getMonths();
int days = p1.getDays();
// 關鍵區別
LocalDateTime dt = LocalDateTime.now();
Duration.between(dt, dt.plusHours(25)); // PT25H
Period.between(dt.toLocalDate(), dt.plusHours(25).toLocalDate()); // P1D (日期變了1天)
// 預定義格式化器
DateTimeFormatter.ISO_LOCAL_DATE // 2024-03-15
DateTimeFormatter.ISO_LOCAL_TIME // 14:30:00
DateTimeFormatter.ISO_LOCAL_DATE_TIME // 2024-03-15T14:30:00
DateTimeFormatter.ISO_ZONED_DATE_TIME // 2024-03-15T14:30:00+08:00[Asia/Shanghai]
DateTimeFormatter.RFC_1123_DATE_TIME // Fri, 15 Mar 2024 14:30:00 GMT
// 自定義格式化器(線程安全,可靜態常量)
static final DateTimeFormatter CUSTOM = DateTimeFormatter
.ofPattern("yyyy-MM-dd HH:mm:ss")
.withZone(ZoneId.systemDefault()); // 可指定時區
static final DateTimeFormatter CHINESE = DateTimeFormatter
.ofPattern("yyyy年MM月dd日 EEEE", Locale.CHINESE);
// 格式化
String formatted = LocalDateTime.now().format(CUSTOM);
// 解析
LocalDateTime parsed = LocalDateTime.parse("2024-03-15 14:30:00", CUSTOM);
ZonedDateTime parsedZoned = ZonedDateTime.parse("2024-03-15 14:30:00+08:00",
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ssXXX"));
// 解析嚴格模式
DateTimeFormatter strict = DateTimeFormatter
.ofPattern("yyyy-MM-dd")
.withResolverStyle(ResolverStyle.STRICT); // 校驗 2月30日等非法日期
// Date → java.time
Date date = new Date();
Instant instant = date.toInstant();
LocalDateTime ldt = instant.atZone(ZoneId.systemDefault()).toLocalDateTime();
ZonedDateTime zdt = instant.atZone(ZoneId.systemDefault());
// Calendar → java.time
Calendar cal = Calendar.getInstance();
ZonedDateTime zdt2 = cal.toInstant().atZone(cal.getTimeZone().toZoneId());
// java.time → Date
Date date2 = Date.from(instant);
Date date3 = Date.from(zdt.toInstant());
// java.time → Calendar
Calendar cal2 = Calendar.from(zdt);
Calendar cal3 = GregorianCalendar.from(zdt);
// 注意:舊 API 只有毫秒精度,納秒信息會丟失
DATETIME / TIMESTAMP → LocalDateTime (無時區) 或 Instant (UTC)TIMESTAMPTZ → OffsetDateTime 或 ZonedDateTime
@Configuration
public class JacksonConfig {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule()); // 關鍵模塊
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); // ISO 字符串
mapper.setTimeZone(TimeZone.getTimeZone("UTC")); // 默認序列化為 UTC
return mapper;
}
}
// 實體類
@Entity
public class Event {
@Column(columnDefinition = "TIMESTAMP")
private Instant createdAt; // 數據庫存 UTC
@Transient
public ZonedDateTime getCreatedAtInUserZone(ZoneId userZone) {
return createdAt.atZone(userZone);
}
}
// 每天凌晨 2 點執行
@Scheduled(cron = "0 0 2 * * ?", zone = "Asia/Shanghai")
public void dailyJob() { ... }
// 精確到瞬間的延遲執行
scheduler.schedule(() -> task.run(),
Instant.now().plus(Duration.ofMinutes(10)));
@Test
void testWithFixedClock() {
Clock fixed = Clock.fixed(Instant.parse("2024-03-15T10:00:00Z"), ZoneId.of("UTC"));
// 注入固定時鐘
service.setClock(fixed);
// 所有 now() 返回固定時間
assertEquals(LocalDate.of(2024, 3, 15), service.getCurrentDate());
}
| 舊代碼 | 新代碼 | 注意 |
|---|---|---|
| new Date() | Instant.now() / LocalDateTime.now() | 明確是否需要時區 |
| Calendar.getInstance() | ZonedDateTime.now() | |
| date.getTime() | instant.toEpochMilli() | |
| new Date(time) | Instant.ofEpochMilli(time) | |
| SimpleDateFormat | DateTimeFormatter | 線程安全,可靜態常量 |
| cal.add(Calendar.DAY, 1) | date.plusDays(1) | 不可變,需賦值 |
| cal.set(2024, 2, 15) | LocalDate.of(2024, 3, 15) | 月份 1-12,無需 -1 |
| cal.get(Calendar.MONTH) | date.getMonthValue() | 1-12 |
| date.before(another) | instant.isBefore(another) | |
| TimeUnit.DAYS.convert(ms, MILLIS) | Duration.ofMillis(ms).toDays() |
// 基準測試 (JMH) 大致結果:
// 創建對象:java.time ≈ Calendar < Date (Date 最快但不安全)
// 格式化:DateTimeFormatter > SimpleDateFormat (單線程) >> SimpleDateFormat (多線程同步)
// 解析:DateTimeFormatter > SimpleDateFormat
// 運算:java.time 不可變對象分配略多,但逃逸分析優化後差異微小
// 總結:java.time 綜合性能優於舊 API,且線程安全
Java 8 java.time 包是現代 Java 日期時間處理的標準。核心原則:優先使用 Instant (存儲/傳輸)、LocalDateTime (業務邏輯)、ZonedDateTime (用戶顯示)、Duration/Period (計算)、DateTimeFormatter (格式化)。遷移舊代碼時注意不可變性、時區語義、線程安全。新項目全面禁用 Date/Calendar/SimpleDateFormat,建立團隊編碼規範。