Java 日期與日曆:從傳統方法到現代方法
1. 概述
處理日期和時間是許多 Java 應用程式的基本部分。多年來,Java 在處理日期方面不斷發展,並引入了更好的解決方案來簡化開發人員的工作。
在本教程中,我們將首先從較舊的類別開始,探索 Java 的日期歷史。然後,我們將繼續採用現代最佳實踐,確保我們能夠自信地處理日期。
2. 傳統方法
在java.time
套件出現之前, Date
和Calendar
類別主要處理日期管理。這些課程很有效,但也有其怪癖。
2.1. java.util.Date
類
java.util.Date
類別是 Java 處理日期的原始解決方案,但它有一些缺點:
- 它是可變的,這意味著我們可能會遇到線程安全問題。
- 不支援時區。
- 它使用令人困惑的方法名稱和傳回值,例如
getYear()
,它會傳回自 1900 年以來的年數。 - 許多方法現在已被棄用。
使用其無參數建構函式建立Date
物件表示當前日期和時間(建立物件的時刻)。讓我們實例化一個Date
物件並列印它的值:
Date now = new Date();
logger.info("Current date and time: {}", now);
這將輸出當前日期和時間,例如Wed Sep 24 10:30:45 PDT 2024
。雖然此構造函數仍然有效,但由於上述原因,不再建議將其用於新專案。
2.2. java.util.Calendar
類
在面臨Date
的限制後,Java 引入了Calendar
類,它提供了改進:
- 支援各種日曆系統
- 時區管理
- 更直觀的日期操作方式
我們也可以使用Calendar
操作日期:
Calendar cal = Calendar.getInstance();
cal.add(Calendar.DAY_OF_MONTH, 5);
Date fiveDaysLater = cal.getTime();
在此範例中,我們計算距離目前日期 5 天的日期並將其儲存在Date
物件中。
但即使是Calendar
也有其缺陷:
- 與
Date
一樣,它仍然是可變的並且不是線程安全的。 - 它的 API 可能會令人困惑和複雜,就像幾個月的從零開始的索引一樣。
3. 現代方法: java.time
套件
在 Java 8 中, java.time
套件出現了,它提供了一個現代、強大的 API 來處理日期和時間。它旨在解決舊版Date
和Calendar
類的許多問題,使日期和時間操作更加直觀和用戶友好。
受到流行的 Joda-Time 庫的啟發, java.time
現在是處理日期和時間的核心 Java 解決方案。
3.1. java.time
中的關鍵類
java.time
套件提供了實際應用程式中經常使用的幾個重要類別。這些類別可以分為三個主要類別:
時間容器:
-
LocalDate
:僅表示日期(不包含時間或時區) -
LocalTime
:表示時間,但不含日期或時區 -
LocalDateTime
:組合日期和時間,但不包含時區 -
ZonedDateTime
:包括日期和時間以及時區 -
Instant
:表示時間線上的特定點,類似時間戳
時間操縱器:
-
Duration
:表示基於時間的時間量(例如“5 小時”或“30 秒”) -
Period
:表示基於日期的時間量(例如「2年3個月」) -
TemporalAdjusters
:提供調整日期的方法(例如查找下週一) -
Clock
:使用時區提供目前日期時間並允許時間控制
格式化程式/印表機:
-
DateTimeFormatter
:用於格式化和解析日期時間對象
3.2. java.time
的優點
java.time
套件對舊的日期和時間類別帶來了一些改進:
- 不變性:所有類別都是不可變的,確保線程安全。
- 清晰的API :方法一致,使API更容易理解。
- 重點類別:每個類別都有特定的角色,無論它是處理儲存日期、操作日期或格式化日期。
- 格式化和解析:內建方法可以輕鬆格式化和解析日期。
4. java.time
的使用範例
在深入研究更進階的功能之前,讓我們先從使用java.time
套件建立日期和時間表示的基礎知識開始。一旦我們有了堅實的基礎,我們將探索如何調整日期以及如何格式化和解析它們。
4.1.建立日期表示
java.time
套件提供了幾個類別來表示日期和時間的不同面向。讓我們使用LocalDate
、 LocalTime
和LocalDateTime
來建立一個基本日期:
@Test
void givenCurrentDateTime_whenUsingLocalDateTime_thenCorrect() {
LocalDate currentDate = LocalDate.now(); // Current date
LocalTime currentTime = LocalTime.now(); // Current time
LocalDateTime currentDateTime = LocalDateTime.now(); // Current date and time
assertThat(currentDate).isBeforeOrEqualTo(LocalDate.now());
assertThat(currentTime).isBeforeOrEqualTo(LocalTime.now());
assertThat(currentDateTime).isBeforeOrEqualTo(LocalDateTime.now());
}
我們也可以透過傳遞所需參數來創建特定的日期和時間:
@Test
void givenSpecificDateTime_whenUsingLocalDateTime_thenCorrect() {
LocalDate date = LocalDate.of(2024, Month.SEPTEMBER, 18);
LocalTime time = LocalTime.of(10, 30);
LocalDateTime dateTime = LocalDateTime.of(date, time);
assertEquals("2024-09-18", date.toString());
assertEquals("10:30", time.toString());
assertEquals("2024-09-18T10:30", dateTime.toString());
}
4.2.使用TemporalAdjusters
調整日期表示
一旦我們有了日期表示,我們就可以使用TemporalAdjusters
來調整它。 TemporalAdjusters
類別提供了一組預先定義的方法來操作日期:
@Test
void givenTodaysDate_whenUsingVariousTemporalAdjusters_thenReturnCorrectAdjustedDates() {
LocalDate today = LocalDate.now();
LocalDate nextMonday = today.with(TemporalAdjusters.next(DayOfWeek.MONDAY));
assertThat(nextMonday.getDayOfWeek())
.as("Next Monday should be correctly identified")
.isEqualTo(DayOfWeek.MONDAY);
LocalDate firstDayOfMonth = today.with(TemporalAdjusters.firstDayOfMonth());
assertThat(firstDayOfMonth.getDayOfMonth())
.as("First day of the month should be 1")
.isEqualTo(1);
}
除了預先定義的調整器之外,我們還可以根據特定需求建立自訂調整器:
@Test
void givenCustomTemporalAdjuster_whenAddingTenDays_thenCorrect() {
LocalDate specificDate = LocalDate.of(2024, Month.SEPTEMBER, 18);
TemporalAdjuster addTenDays = temporal -> temporal.plus(10, ChronoUnit.DAYS);
LocalDate adjustedDate = specificDate.with(addTenDays);
assertEquals(
today.plusDays(10),
adjustedDate,
"The adjusted date should be 10 days later than September 18, 2024"
);
}
4.3.設定日期格式
java.time.format
套件中的DateTimeFormatter
類別允許我們以線程安全的方式格式化和解析日期時間物件:
@Test
void givenDateTimeFormat_whenFormatting_thenVerifyResults() {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy HH:mm");
LocalDateTime specificDateTime = LocalDateTime.of(2024, 9, 18, 10, 30);
String formattedDate = specificDateTime.format(formatter);
LocalDateTime parsedDateTime = LocalDateTime.parse("18-09-2024 10:30", formatter);
assertThat(formattedDate).isNotEmpty().isEqualTo("18-09-2024 10:30");
}
我們可以根據需要使用預先定義的格式或自訂模式。
4.4.解析日期
類似地, DateTimeFormatter
可以將字串表示形式解析回日期或時間物件:
@Test
void givenDateTimeFormat_whenParsing_thenVerifyResults() {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy HH:mm");
LocalDateTime parsedDateTime = LocalDateTime.parse("18-09-2024 10:30", formatter);
assertThat(parsedDateTime)
.isNotNull()
.satisfies(time -> {
assertThat(time.getYear()).isEqualTo(2024);
assertThat(time.getMonth()).isEqualTo(Month.SEPTEMBER);
assertThat(time.getDayOfMonth()).isEqualTo(18);
assertThat(time.getHour()).isEqualTo(10);
assertThat(time.getMinute()).isEqualTo(30);
});
}
4.5.透過OffsetDateTime
和OffsetTime
使用時區
使用不同時區時, OffsetDateTime
和OffsetTime
類別可用於處理日期和時間值或與 UTC 的偏移量:
@Test
void givenVariousTimeZones_whenCreatingOffsetDateTime_thenVerifyOffsets() {
ZoneId parisZone = ZoneId.of("Europe/Paris");
ZoneId nyZone = ZoneId.of("America/New_York");
OffsetDateTime parisTime = OffsetDateTime.now(parisZone);
OffsetDateTime nyTime = OffsetDateTime.now(nyZone);
assertThat(parisTime)
.isNotNull()
.satisfies(time -> {
assertThat(time.getOffset().getTotalSeconds())
.isEqualTo(parisZone.getRules().getOffset(Instant.now()).getTotalSeconds());
});
// Verify time differences between zones
assertThat(ChronoUnit.HOURS.between(nyTime, parisTime) % 24)
.isGreaterThanOrEqualTo(5) // NY is typically 5-6 hours behind Paris
.isLessThanOrEqualTo(7);
}
這裡我們示範如何為不同時區建立OffsetDateTime
實例並驗證它們的偏移量。我們首先使用ZoneId
定義巴黎和紐約的時區。然後,我們使用OffsetDateTime.now()
來捕捉兩個區域的當前時間。
此測試檢查巴黎時間偏移是否與巴黎時區的預期偏移相符。最後,我們驗證紐約和巴黎之間的時差,確保其在 5 到 7 小時的典型範圍內,反映標準時區差異。
4.6.進階用例:時鐘
java.time
套件中的Clock類別提供了一種靈活的方式來存取當前日期和時間,考慮到特定的時區。當我們需要更多地控制時間或測試基於時間的邏輯時,它非常有用。
與使用LocalDateTime.now()
取得系統當前時間不同, Clock允許我們取得相對於特定時區的時間,甚至模擬時間以進行測試。透過將ZoneId
傳遞給Clock.system()
方法,我們可以獲得任何區域的當前時間。例如,在下面的測試案例中,我們使用Clock類別來檢索「America/New_York」時區的當前時間:
@Test
void givenSystemClock_whenComparingDifferentTimeZones_thenVerifyRelationships() {
Clock nyClock = Clock.system(ZoneId.of("America/New_York"));
LocalDateTime nyTime = LocalDateTime.now(nyClock);
assertThat(nyTime)
.isNotNull()
.satisfies(time -> {
assertThat(time.getHour()).isBetween(0, 23);
assertThat(time.getMinute()).isBetween(0, 59);
// Verify it's within last minute (recent)
assertThat(time).isCloseTo(
LocalDateTime.now(),
within(1, ChronoUnit.MINUTES)
);
});
}
這也使得時鐘對於必須管理多個時區或一致地控制時間流的應用程式非常有用。
5.從傳統階級到現代階級的遷移
我們可能仍然需要處理使用Date
或Calendar
的遺留程式碼或函式庫。幸運的是,我們可以從舊的日期時間類別遷移到新的日期時間類別。
5.1.將Date
轉換為Instant
使用toInstant()
方法可以輕鬆地將舊版Date
類別轉換為Instant
類別。當我們遷移到java.time
套件中的類別時,這很有幫助,因為Instant
代表時間軸上的一個點(紀元):
@Test
void givenSameEpochMillis_whenConvertingDateAndInstant_thenCorrect() {
long epochMillis = System.currentTimeMillis();
Date legacyDate = new Date(epochMillis);
Instant instant = Instant.ofEpochMilli(epochMillis);
assertEquals(
legacyDate.toInstant(),
instant,
"Date and Instant should represent the same moment in time"
);
}
我們可以將遺留Date
轉換為Instant
並透過從相同的紀元毫秒創建兩者來確保它們代表同一時刻。
5.2.將Calendar
遷移到ZonedDateTime
使用Calendar
時,我們可以遷移到更現代的ZonedDateTime
,它可以處理日期和時間以及時區資訊:
@Test
void givenCalendar_whenConvertingToZonedDateTime_thenCorrect() {
Calendar calendar = Calendar.getInstance();
calendar.set(2024, Calendar.SEPTEMBER, 18, 10, 30);
ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(
calendar.toInstant(),
calendar.getTimeZone().toZoneId()
);
assertEquals(LocalDate.of(2024, 9, 18), zonedDateTime.toLocalDate());
assertEquals(LocalTime.of(10, 30), zonedDateTime.toLocalTime());
}
在這裡,我們將Calendar
實例轉換為ZonedDateTime
並驗證它們代表相同的日期時間。
6. 最佳實踐
現在讓我們探討一些使用java.time
類別的最佳實務:
- 我們應該對任何新專案使用
java.time
類別。 - 當不需要時區時,我們可以使用
LocalDate
、LocalTime
或LocalDateTime
。 - 使用時區或時間戳記時,請使用
ZonedDateTime
或Instant
。 - 我們應該使用
DateTimeFormatter
來解析和格式化日期。 - 我們應該始終明確時區以避免混淆。
這些最佳實踐為在 Java 中處理日期和時間奠定了堅實的基礎,確保我們能夠在應用程式中有效且準確地處理它們。
七、結論
Java 8 中引入的java.time
套件大大改善了我們處理日期和時間的方式。此外,採用此 API 可確保程式碼更清晰、更易於維護。
雖然我們可能會遇到像Date
或Calendar
這樣的舊類,但採用java.time
API 進行新開發是一個好主意。最後,概述的最佳實踐將幫助我們編寫更乾淨、更有效率、更易於維護的程式碼。
與往常一樣,本文的完整原始碼位於 GitHub 上。