新的日期時間API

本文參考書籍《Java 8實戰》,陸明剛、勞佳  譯,如有侵權,請聯繫刪除!


在Java 1.0中,對日期和時間的支持只能依賴java.util.Date類。正如類名所表達的,這個類無法表示日期,只能以毫秒的精度表示時間。更糟糕的是它的易用性,由於某些原因未知的設計決策,這個類的易用性被深深地損害了,比如:年份的起始選擇是1900年,月份的起始從0開始。這意味着,如果你想要用Date表示Java 8的發佈日期,即2014年3月18日,需要創建下面這樣的Date實例:

    Date date = new Date(114, 2, 18);

它的打印輸出效果爲:

    Tue Mar 18 00:00:00 CET 2014

看起來不那麼直觀。此外,Date類的toString方法返回的字符串也容易誤導人。以我們的例子而言,它的返回值中甚至還包含了JVM的默認時區CET,即中歐時間(CentralEurope Time)。但這並不表示Date類在任何方面支持時區。

隨着Java 1.0退出歷史舞臺, Date類的種種問題和限制幾乎一掃而光,但很明顯,這些歷史舊賬如果不犧牲前向兼容性是無法解決的。所以,在Java 1.1中, Date類中的很多方法被廢棄了,取而代之的是java.util.Calendar類。很不幸, Calendar類也有類似的問題和設計缺陷,導致使用這些方法寫出的代碼非常容易出錯。比如,月份依舊是從0開始計算(不過,至少Calendar類拿掉了由1900年開始計算年份這一設計)。更糟的是,同時存在Date和Calendar這兩個類,也增加了程序員的困惑。到底該使用哪一個類呢?此外,有的特性只在某一個類有提供,比如用於以語言無關方式格式化和解析日期或時間的DateFormat方法就只在Date類裏有。

DateFormat方法也有它自己的問題。比如,它不是線程安全的。這意味着兩個線程如果嘗試使用同一個formatter解析日期,你可能會得到無法預期的結果。

最後, Date和Calendar類都是可以變的。能把2014年3月18日修改成4月18日意味着什麼呢?這種設計會將你拖入維護的噩夢。

所有這些缺陷和不一致導致用戶們轉投第三方的日期和時間庫,比如Joda-Time。爲了解決這些問題, Oracle決定在原生的Java API中提供高質量的日期和時間支持。所以,你會看到Java 8在java.time包中整合了很多Joda-Time的特性。讓我們從探索如何創建簡單的日期和時間間隔入手。java.time包中提供了很多新的類可以幫你解決問題,它們是LocalDate、 LocalTime、 Instant、 Duration和Period。

LocalDate 和 LocalTime

LocalDate類的實例是一個不可變對象,它只提供了簡單的日期,並不含當天的時間信息。另外,它也不附帶任何與時區相關的信息。可以通過靜態工廠方法of創建一個LocalDate實例。 LocalDate實例提供了多種方法來讀取常用的值,比如年份、月份、星期幾等,如下所示:

        LocalDate date = LocalDate.of(2014, 3, 18);
        int year = date.getYear(); // 2014
        Month month = date.getMonth(); // MARCH
        int day = date.getDayOfMonth(); // 18
        DayOfWeek dow = date.getDayOfWeek(); // TUESDAY
        int len = date.lengthOfMonth(); // 31 (days in March)
        boolean leap = date.isLeapYear(); // false (not a leap year,不是閏年)

還可以通過傳遞一個TemporalField參數給get方法拿到同樣的信息。 TemporalField是一個接口,它定義瞭如何訪問temporal對象個字段的值。 ChronoField枚舉實現了這一接口,所以你可以很方便地使用get方法得到枚舉元素的值,如下所示:

        int y = date.get(ChronoField.YEAR);
        int m = date.get(ChronoField.MONTH_OF_YEAR);
        int d = date.get(ChronoField.DAY_OF_MONTH);

可以使用工廠方法從系統時鐘中獲取當前的日期:

    LocalDate today = LocalDate.now();

類似地,一天中的時間,比如13:45:20,可以使用LocalTime類表示。可以使用of重載的兩個工廠方法創建LocalTime的實例。第一個重載函數接收小時和分鐘,第二個重載函數同時還接收秒。同LocalDate一樣, LocalTime類也提供了一些getter方法訪問這些變量的值,如下所示:

        LocalTime time = LocalTime.of(13, 45, 20); // 13:45:20
        int hour = time.getHour(); // 13
        int minute = time.getMinute(); // 45
        int second = time.getSecond(); // 20

LocalDate和LocalTime都可以通過解析代表它們的字符串創建。使用靜態方法parse,可以實現這一目的:

    LocalDate date = LocalDate.parse("2014-03-18");
    LocalTime time = LocalTime.parse("13:45:20")

可以向parse方法傳遞一個DateTimeFormatter(第二個參數),該類的實例定義瞭如何格式化一個日期或者時間對象,它是替換老版java.util.DateFormat的推薦替代品。後面會詳細介紹DateTimeFormatter。一旦傳遞的字符串參數無法被解析爲合法的LocalDate或LocalTime對象,這兩個parse方法都會拋出一個繼承自RuntimeException的DateTimeParseException異常。

LocalDateTime

LocalDateTime,是LocalDate和LocalTime的合體。它同時表示了日期和時間,但不帶有時區信息,可以直接創建,也可以通過合併日期和時間對象構造,如下所示:

        LocalDateTime dt1 = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45, 20); // 2014-03-18 13:45:20
        LocalDateTime dt2 = LocalDateTime.of(date, time);
        LocalDateTime dt3 = date.atTime(13, 45, 20);
        LocalDateTime dt4 = date.atTime(time);
        LocalDateTime dt5 = time.atDate(date);

通過LocalDate和LocalTime的atTime或者atDate方法,向LocalDate傳遞一個時間對象,或者向LocalTime傳遞一個日期對象的方式,可以創建一個LocalDateTime對象。也可以使用LocalDateTime的toLocalDate或者toLocalTime方法,從LocalDateTime中提取LocalDate或者LocalTime:

    LocalDate date1 = dt1.toLocalDate();
    LocalTime time1 = dt1.toLocalTime();

Instant

作爲人,我們習慣於以星期幾、幾號、幾點、幾分這樣的方式理解日期和時間,這種方式對於計算機而言並不容易理解。從計算機的角度來看,建模時間最自然的格式是表示一個持續時間段上某個點的單一大整型數。這也是新的java.time.Instant類對時間建模的方式,它是以Unix元年時間(傳統的設定爲UTC時區1970年1月1日午夜時分)開始所經歷的秒數進行計算。

可以通過向Instant類的靜態工廠方法ofEpochSecond傳遞一個代表秒數的值創建一個該類的實例,例如:

    Instant instant = Instant.ofEpochSecond(44 * 365 * 86400);

靜態工廠方法ofEpochSecond還有一個增強的重載版本,它接收第二個以納秒爲單位的參數值,對傳入作爲秒數的參數進行調整。重載的版本會調整納秒參數,確保保存的納秒分片在0到999 999999之間。這意味着下面這些對ofEpochSecond工廠方法的調用會返回幾乎同樣的Instant對象:

    Instant.ofEpochSecond(3); 
    Instant.ofEpochSecond(3, 0);
    Instant.ofEpochSecond(2, 1000000000); // 2秒之後再加上100萬納秒(1秒)
    Instant.ofEpochSecond(4, -1000000000); // 4秒之前的100萬納秒(1秒)

Instant類也支持靜態工廠方法now,它能夠獲取當前時刻的時間戳:

    Instant now = Instant.now();

特別強調一點, Instant的設計初衷是爲了便於機器使用。它包含的是由秒及納秒所構成的數字。所以,它無法處理那些我們非常容易理解的時間單位。比如下面這段語句:

    int day = Instant.now().get(ChronoField.DAY_OF_MONTH);

它會拋出下面這樣的異常:

    java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: DayOfMonth

Duration 和 Period

目前爲止,我們看到的所有類都實現了Temporal接口。Temporal接口定義瞭如何讀取和操縱爲時間建模的對象的值。之前的介紹中,我們已經瞭解了創建Temporal實例的幾種方法,很自然地我們會想到,我們需要創建兩個Temporal對象之間的duration。 Duration類的靜態工廠方法between就是爲這個目的而設計的。你可以創建兩個LocalTime對象、兩個LocalDateTime對象,或者兩個Instant對象之間的duration,如下所示:

    Duration d1 = Duration.between(time1, time2);
    Duration d1 = Duration.between(dateTime1, dateTime2);
    Duration d2 = Duration.between(instant1, instant2);

如果需要以年、月或者日的方式對多個時間單位建模,可以使用Period類。使用該類的工廠方法between,你可以使用得到兩個LocalDate之間的時長,如下所示:

    Period tenDays = Period.between(LocalDate.of(2014, 3, 8), LocalDate.of(2014, 3, 18));

Duration和Period類都提供了很多非常方便的工廠類,直接創建對應的實例;換句話說,就像下面這段代碼那樣,不再是隻能以兩個temporal對象的差值的方式來定義它們的對象:

    Duration threeMinutes = Duration.ofMinutes(3);
    Duration threeMinutes = Duration.of(3, ChronoUnit.MINUTES);
    Period tenDays = Period.ofDays(10);
    Period threeWeeks = Period.ofWeeks(3);
    Period twoYearsSixMonthsOneDay = Period.of(2, 6, 1);

操縱、解析和格式化日期

如果你已經有一個LocalDate對象,想要創建它的一個修改版,最直接也最簡單的方法是使用withAttribute方法。withAttribute方法會創建對象的一個副本,並按照需要修改它的屬性。注意,下面的這段代碼中所有的方法都返回一個修改了屬性的對象,它們都不會修改原來的對象(它們都是不可變對象)!

    LocalDate date1 = LocalDate.of(2014, 3, 18); // 2014-03-18
    LocalDate date2 = date1.withYear(2011); // 2011-03-18
    LocalDate date3 = date2.withDayOfMonth(25); // 2011-03-25
    LocalDate date4 = date3.with(ChronoField.MONTH_OF_YEAR, 9); // 2011-09-25

採用更通用的with方法能達到同樣的目的,它接受的第一個參數是一個TemporalField對象,第二個參數是修改後的新值,格式類似上面的代碼清單的最後一行。最後這一行中使用的with方法和前面例子中使用的get方法有些類似,它們都聲明於Temporal接口,所有的日期和時間API類都實現這兩個方法,它們定義了單點的時間,比如LocalDate、 LocalTime、 LocalDateTime以及Instant。使用get和with方法,我們可以將Temporal對象值的讀取和修改區分開。如果Temporal對象不支持請求訪問的字段,它會拋出一個UnsupportedTemporalTypeException異常,比如 試 圖 訪 問Instant 對 象 的ChronoField.MONTH_OF_YEAR 字 段 , 或 者LocalDate 對 象 的ChronoField.NANO_OF_SECOND字段時都會拋出這樣的異常。

我們甚至能以聲明的方式操縱LocalDate對象。比如,可以像下面這段代碼那樣加上或者減去一段時間:

    LocalDate date1 = LocalDate.of(2014, 3, 18); // 2014-03-18
    LocalDate date2 = date1.plusWeeks(1); // 2014-03-25
    LocalDate date3 = date2.minusYears(3); // 2011-03-25
    LocalDate date4 = date3.plus(6, ChronoUnit.MONTHS); // 2011-09-25

與get和with方法類似,上面代碼清單中最後一行使用的plus方法也是通用方法,它和minus方法都聲明於Temporal接口中。通過這些方法,對TemporalUnit對象加上或者減去一個數字,我們能非常方便地將Temporal對象前溯或者回滾至某個時間段,通過ChronoUnit枚舉我們可以非常方便地實現TemporalUnit接口。

下表對這些通用的方法進行了總結。

方法名 是否是靜態方法 描述
from 依據傳入的 Temporal 對象創建對象實例
now 依據系統時鐘創建 Temporal 對象
of 由 Temporal 對象的某個部分創建該對象的實例
parse 由字符串創建 Temporal 對象的實例
atOffset 將 Temporal 對象和某個時區偏移相結合
atZone 將 Temporal 對象和某個時區相結合
format 使用某個指定的格式器將Temporal 對象轉換爲字符串(Instant 類不提供該方法)
get 讀取 Temporal 對象的某一部分的值
minus 創建 Temporal 對象的一個副本,通過將當前 Temporal 對象的值減去一定的時長
創建該副本
plus 創建 Temporal 對象的一個副本,通過將當前 Temporal 對象的值加上一定的時長
創建該副本
with 以該 Temporal 對象爲模板,對某些狀態進行修改創建該對象的副本

TemporalAdjuster

截至目前,我們所看到的所有日期操作都是相對比較直接的。有的時候,需要進行一些更加複雜的操作,比如,將日期調整到下個週日、下個工作日,或者是本月的最後一天。這時,你可以使用重載版本的with方法,向其傳遞一個提供了更多定製化選擇的TemporalAdjuster對象,更 加 靈 活 地 處 理 日 期 。 對 於 最 常 見 的 用 例 , 日 期 和 時 間 API 已 經 提 供 了 大 量 預 定 義 TemporalAdjuster。可以通過TemporalAdjusters類的靜態工廠方法訪問它們,如下所示:

    LocalDate date1 = LocalDate.of(2014, 3, 18); // 2014-03-18
    LocalDate date2 = date1.with(nextOrSame(DayOfWeek.SUNDAY)); // 2014-03-23,注意靜態導入
    LocalDate date3 = date2.with(lastDayOfMonth()); // 2014-03-31

下表提供了TemporalAdjusters中包含的工廠方法列表:

方法名 描述
dayOfWeekInMonth 創建一個新的日期,它的值爲同一個月中每一週的第幾天
firstDayOfMonth 創建一個新的日期,它的值爲當月的第一天
firstDayOfNextMonth 創建一個新的日期,它的值爲下月的第一天
firstDayOfNextYear 創建一個新的日期,它的值爲明年的第一天
firstDayOfYear 創建一個新的日期,它的值爲當年的第一天
firstInMonth 創建一個新的日期,它的值爲同一個月中,第一個符合星期幾要求的值
lastDayOfMonth 創建一個新的日期,它的值爲當月的最後一天
lastDayOfNextMonth 創建一個新的日期,它的值爲下月的最後一天
lastDayOfNextYear 創建一個新的日期,它的值爲明年的最後一天
lastDayOfYear 創建一個新的日期,它的值爲今年的最後一天
lastInMonth 創建一個新的日期,它的值爲同一個月中,最後一個符合星期幾要求的值
next/previous 創建一個新的日期,並將其值設定爲日期調整後或者調整前,第一個符合指定星期幾要求的日期
nextOrSame/previousOrSame 創建一個新的日期,並將其值設定爲日期調整後或者調整前,第一個符合指定星期幾要求的日期,如果該日期已經符合要求,直接返回該對象

可見,使用TemporalAdjuster我們可以進行更加複雜的日期操作,而且這些方法的名稱也非常直觀,方法名基本就是問題陳述。此外,即使你沒有找到符合你要求的預定義的TemporalAdjuster,創建你自己的TemporalAdjuster也並非難事。實際上,TemporalAdjuster接口只聲明瞭單一的一個方法(這使得它成爲了一個函數式接口),定義如下:

@FunctionalInterface
public interface TemporalAdjuster {

    Temporal adjustInto(Temporal temporal);

}

這意味着TemporalAdjuster接口的實現需要定義如何將一個Temporal對象轉換爲另一個Temporal對象,可以把它看成一個UnaryOperator<Temporal>,具體如何實現可以參考TemporalAdjusters源碼。

DateTimeFormatter

處理日期和時間對象時,格式化以及解析日期時間對象是另一個非常重要的功能。java.time.format.DateTimeFormatter類就是爲這個目的而設計的。DateTimeFormatter是一個可以替代DateFormat的日期時間格式化器。可以通過它的靜態工廠方法或預定義常量得到DateTimeFormatter的實例。下面的這個例子中,我們使用了兩個不同的格式器生成了字符串:

    LocalDate date = LocalDate.of(2014, 3, 18);
    String s1 = date.format(DateTimeFormatter.BASIC_ISO_DATE); // 20140318
    String s2 = date.format(DateTimeFormatter.ISO_LOCAL_DATE); // 2014-03-18

你也可以通過解析代表日期或時間的字符串重新創建該日期對象。所有的日期和時間API都提供了表示時間點或者時間段的工廠方法,你可以使用工廠方法parse達到重創該日期對象的目的:

    LocalDate date1 = LocalDate.parse("20140318", DateTimeFormatter.BASIC_ISO_DATE);
    LocalDate date2 = LocalDate.parse("2014-03-18", DateTimeFormatter.ISO_LOCAL_DATE);

和java.util.DateFormat相比較,DateTimeFormatter實例都是線程安全的。所以,可以以單例模式創建DateTimeFormatter的實例。 DateTimeFormatter類還支持一個靜態工廠方法,它可以按照某個特定的模式創建DateTimeFormatter實例,代碼清單如下:

    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
    LocalDate date1 = LocalDate.of(2014, 3, 18);
    String formattedDate = date1.format(formatter); // 13/03/2014
    LocalDate date2 = LocalDate.parse(formattedDate, formatter);

ofPattern方法也提供了一個重載的版本,使用它你可以創建某個Locale的格式器,代碼清單如下所示:

    DateTimeFormatter italianFormatter = DateTimeFormatter.ofPattern("d. MMMM yyyy", Locale.ITALIAN);
    LocalDate date1 = LocalDate.of(2014, 3, 18);
    String formattedDate = date.format(italianFormatter); // 18. marzo 2014
    LocalDate date2 = LocalDate.parse(formattedDate, italianFormatter);

時區

前面我們看到的日期和時間的種類都不包含時區信息。時區的處理是新版日期和時間API新增加的重要功能,使用新版日期和時間API時區的處理被極大地簡化了。新的java.time.ZoneId類是老版java.util.TimeZone的替代品。它的設計目標就是要讓你無需爲時區處理的複雜和繁瑣而操心,比如處理日光時(Daylight Saving Time, DST)這種問題。跟其他日期和時間類一樣, ZoneId類也是無法修改的。

時區是按照一定的規則將區域劃分成的標準時間相同的區間。在ZoneRules這個類中包含了40個這樣的實例。你可以簡單地通過調用ZoneId的getRules()得到指定時區的規則。每個特定的ZoneId對象都由一個地區ID標識,比如:

    ZoneId romeZone = ZoneId.of("Europe/Rome");

地區ID都爲“{區域}/{城市}”的格式,這些地區集合的設定都由英特網編號分配機構(IANA)的時區數據庫提供。可以通過Java 8的新方法toZoneId將一個老的時區對象轉換爲ZoneId:

    ZoneId zoneId = TimeZone.getDefault().toZoneId();

一旦得到一個ZoneId對象,就可以將它與LocalDate、 LocalDateTime或者是Instant對象整合起來,構造一個ZonedDateTime實例,它代表了相對於指定時區的時間點,代碼清單如下所示:

    LocalDate date = LocalDate.of(2014, Month.MARCH, 18);
    ZonedDateTime zdt1 = date.atStartOfDay(romeZone);
    LocalDateTime dateTime = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45);
    ZonedDateTime zdt2 = dateTime.atZone(romeZone);
    Instant instant = Instant.now();
    ZonedDateTime zdt3 = instant.atZone(romeZone);

下圖對ZonedDateTime的組成部分進行了說明,相信能夠幫助你理解LocaleDate、LocalTime、 LocalDateTime以及ZoneId之間的差異。

通過ZoneId,你還可以將LocalDateTime轉換爲Instant:

    LocalDateTime dateTime = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45);
    Instant instantFromDateTime = dateTime.toInstant(romeZone);

你也可以通過反向的方式得到LocalDateTime對象:

    Instant instant = Instant.now();
    LocalDateTime timeFromInstant = LocalDateTime.ofInstant(instant, romeZone);

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章