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

Java 日期時間處理大全:從 Date 到 LocalDateTime 完整指南

     編輯:Java 相關知識

前言

Java 日期時間 API 經歷了三代演進:java.util.Date (1.0) → Calendar (1.1) → java.time (Java 8+)。本文系統梳理三代 API 用法、線程安全問題、格式化解析、時區處理、期間計算、最佳實踐遷移指南。

一、三代 API 對比

特性Date (1.0)Calendar (1.1)java.time (8+)
線程安全❌ 可變❌ 可變✅ 不可變
API 設計混亂、缺陷多複雜、易錯清晰、領域驅動
時區支持弱一般強 (ZoneId/ZoneOffset)
期間/持續時間無無Period/Duration
格式化/解析SimpleDateFormat (非線程安全)同左DateTimeFormatter (線程安全)
推薦程度❌ 棄用⚠️ 維舊✅ 首選

二、java.time 核心類圖譜


Instant          時間線上的瞬間 (UTC,納秒精度)
    │
    ├── LocalDate      本地日期 (年月日,無時區)
    ├── LocalTime      本地時間 (時分秒納秒,無時區)
    ├── LocalDateTime  本地日期時間 (無時區)
    │
    ├── ZonedDateTime  帶時區日期時間 (最完整)
    ├── OffsetDateTime 帶偏移量日期時間
    ├── OffsetTime     帶偏移量時間
    │
    ├── Duration       基於時間的量 (秒/納秒,適合 Instant)
    ├── Period         基於日期的量 (年月日,適合 LocalDate)
    │
    ├── ZoneId         時區 ID (如 Asia/Shanghai)
    ├── ZoneOffset     時區偏移 (如 +08:00)
    │
    ├── DateTimeFormatter  格式化/解析 (線程安全)
    ├── TemporalAdjusters  日期調整器 (下一個周一、月末等)
    └── Clock            時鐘 (可注入,便於測試)

三、核心類實戰用法

1. Instant:時間戳替代者


// 獲取當前瞬間
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);

2. LocalDate / LocalTime / LocalDateTime


// 創建
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();

3. ZonedDateTime:時區感知


// 創建
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

4. Duration vs Period


// 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天)

5. DateTimeFormatter:線程安全格式化


// 預定義格式化器
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日等非法日期

四、與舊 API 互操作


// 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 只有毫秒精度,納秒信息會丟失

五、常見場景最佳實踐

1. 數據庫存儲

  • MySQL DATETIME / TIMESTAMP → LocalDateTime (無時區) 或 Instant (UTC)
  • PostgreSQL TIMESTAMPTZ → OffsetDateTime 或 ZonedDateTime
  • 統一策略:數據庫存 UTC (Instant),業務層轉換為用戶時區 (ZonedDateTime)

2. JSON 序列化 (Jackson)


@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);
    }
}

3. 定時任務


// 每天凌晨 2 點執行
@Scheduled(cron = "0 0 2 * * ?", zone = "Asia/Shanghai")
public void dailyJob() { ... }

// 精確到瞬間的延遲執行
scheduler.schedule(() -> task.run(), 
    Instant.now().plus(Duration.ofMinutes(10)));

4. 測試中的時間控制


@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());
}

六、遷移清單:從 Date/Calendar 到 java.time

舊代碼新代碼注意
new Date()Instant.now() / LocalDateTime.now()明確是否需要時區
Calendar.getInstance()ZonedDateTime.now()
date.getTime()instant.toEpochMilli()
new Date(time)Instant.ofEpochMilli(time)
SimpleDateFormatDateTimeFormatter線程安全,可靜態常量
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()

七、常見坑點

  1. LocalDateTime 不含時區:跨時區傳輸必須用 ZonedDateTime/OffsetDateTime/Instant
  2. DateTimeFormatter 非線程安全?:是線程安全的,可作為靜態常量複用
  3. Period vs Duration:日期間隔用 Period,時間間隔用 Duration
  4. withZoneSameInstant vs withZoneSameLocal:前者瞬間不變,後者本地時間不變
  5. 解析嚴格模式:生產環境建議 `ResolverStyle.STRICT`
  6. 序列化為時間戳:Jackson 默認寫數字,建議關閉 `WRITE_DATES_AS_TIMESTAMPS`
  7. 數據庫時區:確認連接字符串 `serverTimezone=UTC` 或應用層統一轉換

八、性能對比


// 基準測試 (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,建立團隊編碼規範。

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