Java 8 的時間日期 API

YangAM發表於2018-04-23

上一篇文章『Java 的時間日期 API』中,我們學習了由 Date、Calendar,DateFormat 等組成的「傳統時間日期 API」,但是傳統的處理介面設計並不是很友好,不易使用。終於,Java 8 借鑑第三方優秀開源庫 Joda-time,重新設計了一套 API。

那麼本篇文章就來簡單學習一下新式的時間日期處理介面。

表示時刻的 Instant

Instant 和 Date 一樣,表示一個時間戳,用於描述一個時刻,只不過它較 Date 而言,可以描述更加精確的時刻。並且 Instant 是時區無關的。

Date 最多可以表示毫秒級別的時刻,而 Instant 可以表示納秒級別的時刻。例如:

  • public static Instant now():根據系統當前時間建立一個 Instant 例項,表示當前時刻
  • public static Instant ofEpochSecond(long epochSecond):通過傳入一個標準時間的偏移值來構建一個 Instant 例項
  • public static Instant ofEpochMilli(long epochMilli):通過毫秒數值直接構建一個 Instant 例項

看看程式碼:

public static void main(String[] args){
    //建立 Instant 例項
    Instant instant = Instant.now();
    System.out.println(instant);

    Instant instant1 = Instant.ofEpochSecond(20);
    System.out.println(instant1);

    Instant instant2 = Instant.ofEpochSecond(30,100);
    System.out.println(instant2);

    Instant instant3 = Instant.ofEpochMilli(1000);
    System.out.println(instant3);
}
複製程式碼

輸出結果:

2018-04-23T02:43:10.973Z
1970-01-01T00:00:20Z
1970-01-01T00:00:30.000000100Z
1970-01-01T00:00:01Z
複製程式碼

可以看到,Instant 和 Date 不同的是,它是時區無關的,始終是格林零時區相關的,也即是輸出的結果始終格林零時區時間。

處理日期的 LocalDate

不同於 Calendar 既能處理日期又能處理時間,java.time 的新式 API 分離開日期和時間,用單獨的類進行處理。LocalDate 專注於處理日期相關資訊。

LocalDate 依然是一個不可變類,它關注時間中年月日部分,我們可以通過以下的方法構建和初始化一個 LocalDate 例項:

  • public static LocalDate now():截斷當前系統時間的年月日資訊並初始化一個例項物件
  • public static LocalDate of(int year, int month, int dayOfMonth):顯式指定年月日資訊
  • public static LocalDate ofYearDay(int year, int dayOfYear):根據 dayOfYear 可以推出 month 和 dayOfMonth
  • public static LocalDate ofEpochDay(long epochDay):相對於格林零時區時間的日偏移量

看看程式碼:

public static void main(String[] args){
    //構建 LocalDate 例項
    LocalDate localDate = LocalDate.now();
    System.out.println(localDate);

    LocalDate localDate1 = LocalDate.of(2017,7,22);
    System.out.println(localDate1);

    LocalDate localDate2 = LocalDate.ofYearDay(2018,100);
    System.out.println(localDate2);

    LocalDate localDate3 = LocalDate.ofEpochDay(10);
    System.out.println(localDate3);
}
複製程式碼

輸出結果:

2018-04-23
2017-07-22
2018-04-10
1970-01-11
複製程式碼

需要注意一點,LocalDate 會根據系統中當前時刻和預設時區計算出年月日的資訊。

除此之外,LocalDate 中還有大量關於日期的常用方法:

  • public int getYear():獲取年份資訊
  • public int getMonthValue():獲取月份資訊
  • public int getDayOfMonth():獲取當前日是這個月的第幾天
  • public int getDayOfYear():獲取當前日是這一年的第幾天
  • public boolean isLeapYear():是否是閏年
  • public int lengthOfYear():獲取這一年有多少天
  • public DayOfWeek getDayOfWeek():返回星期資訊
  • 等等

這些方法都見名知意,此處不再贅述。

處理時間的 LocalTime

類似於 LocalDate,LocalTime 專注於時間的處理,它提供小時,分鐘,秒,毫微秒的各種處理,我們依然可以通過類似的方式建立一個 LocalTime 例項。

  • public static LocalTime now():根據系統當前時刻獲取其中的時間部分內容
  • public static LocalTime of(int hour, int minute):顯式傳入小時和分鐘來構建一個例項物件
  • public static LocalTime of(int hour, int minute, int second):通過傳入時分秒構造例項
  • public static LocalTime of(int hour, int minute, int second, int nanoOfSecond):傳入時分秒和毫微秒構建一個例項
  • public static LocalTime ofSecondOfDay(long secondOfDay):傳入一個長整型數值代表當前日已經過去的秒數
  • public static LocalTime ofNanoOfDay(long nanoOfDay):傳入一個長整型代表當前日已經過去的毫微秒數

同樣的,LocalTime 預設使用系統預設時區處理時間,看程式碼:

public static void main(String[] a){
    LocalTime localTime = LocalTime.now();
    System.out.println(localTime);

    LocalTime localTime1 = LocalTime.of(23,59);
    System.out.println(localTime1);

    LocalTime localTime2 = LocalTime.ofSecondOfDay(10);
    System.out.println(localTime2);
}
複製程式碼

輸出結果:

13:59:03.723
23:59
00:00:10
複製程式碼

當然,LocalTime 中也同樣封裝了很多好用的工具方法,例如:

  • public int getHour()
  • public int getMinute()
  • public int getSecond()
  • public int getNano()
  • public LocalTime withHour(int hour):修改當前 LocalTime 例項中的 hour 屬性並重新返回一個新的例項
  • public LocalTime withMinute(int minute):類似
  • public LocalTime withSecond(int second)
  • 等等

LocalDateTime 類則是整合了 LocalDate 和 LocalTime,它既能表示日期,又能表述時間資訊,方法都類似,只是有一部分涉及時區的轉換內容,我們待會說。

時區相關的日期時間處理 ZonedDateTime

無論是我們的 LocalDate,或是 LocalTime,甚至是 LocalDateTime,它們基本是時區無關的,內部並沒有儲存時區屬性,而基本用的系統預設時區。往往有些場景之下,缺乏一定的靈活性。

ZonedDateTime 可以被理解為 LocalDateTime 的外層封裝,它的內部儲存了一個 LocalDateTime 的例項,專門用於普通的日期時間處理。此外,它還定義了 ZoneId 和 ZoneOffset 來描述時區的概念。

ZonedDateTime 和 LocalDateTime 的一個很大的不同點在於,後者內部並沒有儲存時區,所以對於系統的依賴性很強,往往換一個時區可能就會導致程式中的日期時間不一致。

而後者則可以通過傳入時區的名稱,使用 ZoneId 進行匹配儲存,也可以通過傳入與零時區的偏移量,使用 ZoneOffset 儲存時區資訊。

所以,構建一個 ZonedDateTime 例項有以下幾種方式:

  • public static ZonedDateTime now():系統將以預設時區計算並儲存日期時間資訊
  • public static ZonedDateTime now(ZoneId zone):指定時區
  • public static ZonedDateTime of(LocalDate date, LocalTime time, ZoneId zone):指定日期時間和時區
  • public static ZonedDateTime of(LocalDateTime localDateTime, ZoneId zone)
  • public static ZonedDateTime ofInstant(Instant instant, ZoneId zone):通過時刻和時區構建例項物件
  • 等等

看程式碼:

public static void main(String[] a){
    ZonedDateTime zonedDateTime = ZonedDateTime.now();
    System.out.println(zonedDateTime);

    LocalDateTime localDateTime = LocalDateTime.now();
    ZoneId zoneId = ZoneId.of("America/Los_Angeles");
    ZonedDateTime zonedDateTime1 = ZonedDateTime.of(localDateTime,zoneId);
    System.out.println(zonedDateTime1);

    Instant instant = Instant.now();
    ZoneId zoneId1 = ZoneId.of("GMT");
    ZonedDateTime zonedDateTime2 = ZonedDateTime.ofInstant(instant,zoneId1);
    System.out.println(zonedDateTime2);
}
複製程式碼

輸出結果:

2018-04-23T16:10:29.510+08:00[Asia/Shanghai]
2018-04-23T16:10:29.511-07:00[America/Los_Angeles]
2018-04-23T08:10:29.532Z[GMT]
複製程式碼

簡單解釋一下,首先第一個輸出應該沒什麼問題,系統儲存當前系統日期和時間以及預設的時區。

第二個小例子,LocalDateTime 例項儲存了時區無關的當前日期時間資訊,也就是這裡的年月日時分秒,接著構建一個 ZonedDateTime 例項並傳入一個美國時區(西七區)。你會發現輸出的日期時間為西七區的 16 點 29 分。

像這種關聯了時區的日期時間就很能夠解決那種,換時區導致程式中時間錯亂的問題。因為我關聯了時區,無論你程式換到什麼地方執行了,日期+時區 本就已經唯一確定了某個時刻,就相當於我在儲存某個時刻的時候,我說明了這是某某時區的某某時間,即便你換了一個地區,你也不至於把這個時間按自己當前的時區進行解析並直接使用了吧。

第三個小例子就更加的直接明瞭了,構建 ZonedDateTime 例項的時候,給定一個時刻和一個時區,而這個時刻值就是相對於給定時區的標準時間所經過的毫秒數。

有關 ZonedDateTime 的其他日期時間的處理方法和 LocalDateTime 是一樣的,因為 ZonedDateTime 是直接封裝了一個 LocalDateTime 例項物件,所以所有相關日期時間的操作都會間接的呼叫 LocalDateTime 例項的方法,我們不再贅述。

格式化日期時間

Java 8 的新式日期時間 API 中,DateTimeFormatter 作為格式化日期時間的主要類,它與之前的 DateFormat 類最大的不同就在於它是執行緒安全的,其他的使用上的操作基本類似。我們看看:

public static void main(String[] a){
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss");
    LocalDateTime localDateTime = LocalDateTime.now();
    System.out.println(formatter.format(localDateTime));

    String str = "2008年08月23日 23:59:59";
    DateTimeFormatter formatter2 = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss");
    LocalDateTime localDateTime2 = LocalDateTime.parse(str,formatter2);
    System.out.println(localDateTime2);

}
複製程式碼

輸出結果:

2018年04月23日 17:27:24
2008-08-23T23:59:59
複製程式碼

格式化主要有兩種情況,一種是將日期時間格式化成字串,另一種則是將格式化的字串裝換成日期時間物件。

DateTimeFormatter 提供將 format 方法將一個日期時間物件轉換成格式化的字串,但是反過來的操作卻建議使用具體的日期時間類自己的 parse 方法,這樣可以省去型別轉換的步驟。

時間差

現實專案中,我們也經常會遇到計算兩個時間點之間的差值的情況,最粗暴的辦法是,全部幻化成毫秒數並進行減法運算,最後在轉換回日期時間物件。

但是 java.time 包中提供了兩個日期時間之間的差值的計算方法,我們一起看看。

關於時間差的計算,主要涉及到兩個類:

  • Period:處理兩個日期之間的差值
  • Duration:處理兩個時間之間的差值

例如:

public static void main(String[] args){
    LocalDate date = LocalDate.of(2017,7,22);
    LocalDate date1 = LocalDate.now();
    Period period = Period.between(date,date1);
    System.out.println(period.getYears() + "年" +
            period.getMonths() + "月" +
            period.getDays() + "天");

    LocalTime time = LocalTime.of(20,30);
    LocalTime time1 = LocalTime.of(23,59);
    Duration duration = Duration.between(time,time1);
    System.out.println(duration.toMinutes() + "分鐘");
}
複製程式碼

輸出結果:

0年9月1天
209分鐘
複製程式碼

顯然,年月日的日期間差值的計算使用 Period 類足以,而時分秒毫秒的時間的差值計算則需要使用 Duration 類。

最後,關於 java.time 包下的新式日期時間 API,我們簡單的學習了下,並沒有深入到原始碼實現層次進行介紹,因為底層涉及大量的系統介面,涉及到大量的抽象類和實現類,有興趣的朋友可以自行閱讀 jdk 的原始碼深入學習。


文章中的所有程式碼、圖片、檔案都雲端儲存在我的 GitHub 上:

(https://github.com/SingleYam/overview_java)

歡迎關注微信公眾號:撲在程式碼上的高爾基,所有文章都將同步在公眾號上。

image

相關文章