(4)使用 JDK8 日期時間 API

Joda-Time 的創建者 Stephen Colebourne 參與了 JSR310,也就是 Java 標準的日期與時間 API 規格之制訂,預計在 JDK8 中一併釋出,為什麼 Stephen Colebourne 不直接將 Joda-Time 放入 Java 標準呢?在他的 Why JSR-310 isn’t Joda-Time 中作了解釋,最主要的是 Stephen Colebourne 認為 Joda-Time 有一些設計上欠周詳的缺點:html

  • 人類與機器的時間軸
  • 可抽換的年曆設計
  • Nulls
  • 內部實做

如下逐一來探討,並看看 JSR310 中會怎麼改正 …java

避免 Nulls

Joda-Time 中有些 API 接受 null,視 API 而定,可能將 null 視為 1970 年 1 月 1 日,或是者是視為 0。null 引發的問題能夠參考 補救 null 的策略;JSR310 的 API 不接受 null。git

清楚區隔人類與機器時間概念

人類與機器對時間的觀點大相徑庭。對機器來說,時間就是不斷增長的數字,以 Java 來說,就是 January 1, 1970, 00:00:00 GMT(實際上是 UTC)經過的毫秒數;對人類來說,對時間的概念有年曆,有年、月、日、時、分、秒,還加上了時區等概念。github

在 Joda-Time 中,DateTime 實做了 ReadableInstant,ReadableInstant 是機器對時間的概念,然而 DateTime 卻是人類對時間的概念,Stephen Colebourne 認為應該將兩種概念予以分離。api

在 JSR310 中,特地讓機器與人類對時間概念的界線變得分明。JSR310 的套件命名從 java.time 開始。對於機器相關的時間概念,JSR310 設計了 final 的 Instant,表明著從 Java epoch(1970 年 1 月 1 日)之後的某個時間點,精確度則可至奈秒(nanosecond)等級。為了避免時間定義上的模糊,JSR310 定義了本身的時間度量(Time-scale) ,能夠在 Instant 的 API 文件 查詢得知其如何定義時間。oracle

對於人類的時間概念,像是日期與時間,JSR310 有 LocalDateTime、LocalDate、LocalTime 等類別來定義,這些類別基於 ISO-8601 年曆系統,是不具時區的日期與時間表明(看 Local 字眼也知道是這樣)。年、月、日的概念,則分別有 Year、YearMonth、MonthDay 等類別,可分別表明如 2007 年、2007-十二、12-03 這樣的概念。.net

對於時間的量,Joda-Time 有 Duration 的概念,JSR310 中也有,以類別 Duration 來定義,用來表示時間方面的量,精度設定能夠達奈秒等級,而秒的最大值能夠是 long 型態可保存之值。Joda-Time 有 Period 的概念,JSR310 也有,以類別 Period 定義,用來表示日期方面的量,像是 2 年、3 個月、4 天等。code

能夠發表,Joda-Time 中的一些概念,經過調整後,依舊可對應至 JSR310,程式碼使用上也類似,來看看實際的程式碼範例。底下是 Joda-Time 中要取得兩個日期間經過幾年的程式碼:orm

Years years = Years.yearsBetween(
DateTime.parse("1975-05-26"), DateTime.now());
System.out.printf("你今年的歲數為:%d%n", years.getYears());

改爲 JSR310 的話,長得也蠻類似的:htm

Period period = Period.between(LocalDate.parse("1975-05-26"), LocalDate.now());
System.out.printf("你今年的歲數為:%d%n", period.getYears());

Joda-Time 中以建構 LocalDate 來表示本地時間:

LocalDate javaTwoDate = new LocalDate(2013, 8, 2);
System.out.printf("Taiwan Java Developer Day is %s.%n", javaTwoDate);

JSR310 中常見到工廠方法創建相關實例:

System.out.printf("Taiwan Java Developer Day is %s.%n", LocalDate.of(2013, 8, 2));

Joda-Time 中對日期進行運算的例子是這樣的:

LocalDate birthDate = new LocalDate(1975, 5, 26);
System.out.println(birthDate
                    .plusDays(5)
                    .plusMonths(6)
                    .plusWeeks(3).toString("E MM/dd/yyyy"));

透過 Joda-Time 中 Period 類別上的 static 方法,搭配 import static,能夠達到更進一步的可讀性:

LocalDate birthDate = new LocalDate(1975, 5, 26);
System.out.println(birthDate
                    .plus(days(5))
                    .plus(months(6))
                    .plus(weeks(3)).toString("E MM/dd/yyyy"));

這是因為 LocalDate 的 plus 方法接受 ReadablePeriod 實例,操做後傳回 LocalDate,於是能夠流暢地持續操做。

在 JSR310 中,則能夠寫成這樣:

LocalDate birthDate = LocalDate.of(1975, 5, 26);
      System.out.println(birthDate
                    .plus(5, DAYS)
                    .plus(6, MONTHS)
                    .plus(3, WEEKS).format(ofPattern("E MM/dd/yyyy")));

JSR310 中,UTC 偏移量與時區的概念是分開的。OffsetDateTime 單純表明 UTC 偏移量,使用 ISO-8601;ZonedDateTime 是表明加入了時區規則的類別。舉例來說,若是有個機器時間觀點的 Instant 實例,你能夠用它來分別取得 UTC 偏移量或者是某時區的時間:

Instant now = Instant.now();
OffsetDateTime offsetDateTime = now.atOffset(ZoneOffset.UTC);
ZonedDateTime zonedDateTime = now.atZone(ZoneId.of("Asia/Taipei"));

類似地,若是有個人類時間概念的 LocalDate 或 LocalTime,也能夠在分別補齊欄位資訊後,分別取得 UTC 偏移量或者是某時區的時間:

LocalDate nowDate = LocalDate.now();
LocalTime nowTime = LocalTime.now();
 
OffsetDateTime offsetDateTime = OffsetDateTime.of(nowDate, nowTime, ZoneOffset.UTC);
ZonedDateTime zonedDateTime = ZonedDateTime.of(nowDate, nowTime, ZoneId.of("Asia/Taipei"));

改善內部實做彈性

Joda-Time 有些實做上缺少彈性或是複雜。舉例而言,若是你仔細察看過 Joda-Time 的 API,能夠發現有些操做在各類別重複了,像是 plus 方法,你能夠在 DateTime、Period 上分別發現 plus 名稱的方法,分別傳回 DateTime、Period 實例,這類 API 上的操做直接定義在類別,將來要擴充時會比較沒有彈性。

JSR310 將 API 上的操做抽取出來獨立定義,放置在 java.time.temporal 套件之中,其中 TemporalAccessor 定義了惟讀用的時間物件(像是日期、時間、偏移量等)讀取操做,Temporal 是 TemporalAccessor 子介面,增長了對時間的處理操做,像是 plus、minus、with 等方法,方纔你看過的 JSR310 相關類別,幾乎都有實做 Temporal 介面,像是 …

  • Instant
  • LocalDate、LocalDateTime、LocalTime
  • OffsetDateTime、OffsetTime
  • Year、YearMonth
  • ZonedDateTime

有趣的是,MonthDay 是惟讀的,也就是僅實做了 TemporalAccessor 介面,為什麼呢?在 MonthDay 的 API 文件 有說明,因為有閏年問題,在缺乏「年」的資訊下,若是 MonthDay 可進行 plus 操做,那麼 2 月 28 日加一天會是 2 月 29 日或是 3 月 1 日就無法定義了…

來看看 Temporal 介面定義的幾個操做:

  • plus(TemporalAmount amount)
  • plus(long amountToAdd, TemporalUnit unit)
  • minus(TemporalAmount amount)
  • minus(long amountToSubtract, TemporalUnit unit)

操做時必須有時間的量,這是由 TemporalAmount 定義,實際上方纔看過 JSR310 中的 Duration、Period 類別,都實做了 TemporalAmount;若是不使用 TemporalAmount 實例,那也能夠指定數字配合時間單位,也就是 TemporalUnit 列舉的單位:

若是隻是想調整某個日期或時間欄位,能夠使用 Temporal 的 with 方法,像是 with(TemporalField field, long newValue),TemporalField 列舉了一些欄位:

若是你須要更複雜的調整,能夠使用 Twith(TemporalAdjuster adjuster),細節可參考 TemporalAdjuster 的 API 文件。

單一年曆系統設計

內部實做除了上述問題以外,也有年曆系統複雜及容易引發誤用的問題,Stephen Colebourne 如下列程式碼為例,month 結果多是 1 ~ 12,但也有多是 1 ~ 13:

int month = dateTime.getMonthOfDay();

若是 dateTime 參考的 DateTime 實例中,實際上若採用了科普特曆(Coptic calendar)的 CopticChronology 實例,傳回值就有多是 1 ~ 13,若是你一直想著用 1 ~ 12 的結果去進行後續運算,就有可能出錯,因為你沒有去確定過使用的是否是 ISO 年歷系統。

JSR310 採單一年曆系統設計,也就是說,事實上 java.time 套件中的類別在須要採行年曆系統時,其實都是採用單一的 ISO-8601 年曆系統;那麼,若是須要其餘年曆系統呢?你不能像 Joda-Time 中進行抽換,而須要明確採行 java.time.chrono 中的相關類別,JapaneseChronology、ThaiBuddhistChronology
等實做了 Chronology 介面的類別,能夠做為使用的起點。

總結

簡單來說,使用 JDK 現有的 Date、Calendar 等既存的日期時間 API,容易出錯、痛苦且麻煩,日期時間在處理時的複雜度,也遠超過日常人們的想像,在處理時間以前,得想一想現在想處理的是機器上的時間概念,還是人類對時間的概念,在 Java 這塊的話,最好是選用個 Joda-Time 或 JSR310,處理上會比較容易。

不單只是 Java 會面臨【Joda-Time 與 JSR310 】系列中談到的問題,其餘語言生態系在處理日期時間時,也會遇到類似問題,如下是一些剛好我有看過的替代程式庫參考:

Date4j:對 java.util.Date 的簡單替代方案
Arrow:Python 中更好的日期與時間處理程式庫
Moment.js:JavaScript 中的日期程式庫
Noda-Time:.NET 陣營對 Joda-Time 的複刻

如下是這系列在準備過程中,一些能夠參考的文件來源:

相關文章
相關標籤/搜索