• (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 有一些設計上欠周詳的缺點:

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

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

    避免 Nulls

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

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

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

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

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

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

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

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

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

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

    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 的複刻

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

  • 相关阅读:
    对于数据的测试
    绕过前端,直接调用后端接口的可能性
    API接口自动化之3 同一个war包中多个接口做自动化测试
    API接口自动化之2 处理http请求的返回体,对返回体做校验
    API接口自动化之1 常见的http请求
    DB中字段为null,为空,为空字符串,为空格要怎么过滤取出有效值
    Linux 常用的压缩命令有 gzip 和 zip
    SQL 常用的命令
    JVM内存管理的机制
    Linux 常见命令
  • 原文地址:https://www.cnblogs.com/aprz512/p/5622957.html
Copyright © 2020-2023  润新知