JSR 310 是一個用於執行與時間和日歷有關的計算的 API,已經得到 Java SE 7 的推薦。該 API 的 目標是取代現有的兩個構成 Java 的當前日期和時間 API 的類:java.util.Date 和 java.util.Calendar,同時仍然提供對這些舊有 API 的向後兼容訪問。JSR 當前正在開發,並且該 API 有一個可用的試驗性 Javadoc。
對 Java 6 日期/時間 API 的改進
JSR 310 日期/時間 API 試圖通過提供更好的性能和易用性改進 Java 的當前日期/時間 API。例如, Java Calendar 類將日期同時存儲為與標准紀元之間的偏移量(以毫秒為單位)以及一組日歷字段(例如 ,星期幾、幾號以及月份)。此雙精度表示導致在意外的時間重新計算日歷字段,從而產生不可預測的性 能特點。與此相比,JSR 310 類僅將日期/時間表示存儲為與 Date 和 Calendar 所使用的同一標准紀元 之間的偏移量(以毫秒為單位);僅當需要時才會計算日期等日歷字段,並且不會使用這些日歷字段進行 內部日期表示。
JSR 310 還對當前 Java 日期/時間模型進行了改進。Java 6 API 不包含表示本地時間(不具有關聯 時區的時間)、持續時間或時間間隔的類。這迫使程序員使用令人困惑的設計做法,例如使用 int 表示 持續時間。JSR 310 包含表示上述各個概念的類,從而可以進行更為明確的程序設計。
最後,JSR 310 API 通過使用不可改變的類努力實現線程安全。Java 當前的日期/時間類 Date 和 Calendar 都是可改變的,因而都不是線程安全的。
JSR 310 日期/時間概念
JSR 310 API 利用了從多個第三方 Java 日期/時間 API 中獲得的經驗。JSR 310 主要基於 Joda Time API;其他影響因素包括 ICU、Time and Money 和 Calendar Date。JSR 310 的 API 是圍繞 Joda Time 中使用的相同 5 個基本日期/時間概念構建的:
離散的時間線
瞬間
不完全時間
持續時間
時段
時間間隔
離散的時間線
像 Joda Time 一樣,JSR 310 使用離散化的時間線:時間被建模為由小的固定持續時間分隔的連續的 瞬間序列。JSR 310 的離散時間線具有納秒分辨率,因此可以表示時間“2008 年 1 月 1 日午夜後 1 納 秒”,但不能表示時間“2008 年 1 月 1 日午夜後 1 皮秒”。該時間線上的每個離散納秒都被視為一個 瞬間,如下所示。
瞬間
瞬間是離散化時間線上的特定點。一個瞬間示例是“世界標准時間 2008 年 1 月 7 日 23:00:00.0” 。同樣可以將瞬間定義為與標准紀元之間的偏移量(以納秒為單位),例如“世界標准時間 1970 年 1 月 1 日之後 20,000,000 納秒”。這兩個說明都定義了離散時間線上的單個唯一點。瞬間不同於不完全 時間,後者定義了時間線上的一組時刻,而不是一個唯一的時刻。
JSR 310 API 提供了多個表示瞬間的類:Instant、OffsetDateTime 和 ZonedDateTime,所有這些類 都實現了 ReadableInstant 接口。OffsetDateTime 類表示日期、每日時間以及與世界標准時間 (coordinated universal time, UTC)之間的偏移量(如 +1:00)。類似的 ZonedDateTime 類包含時區 ID(如 America/New_York)而不是偏移量。給定的時區可能使用多個不同的偏移量,具體取決於一年中 的時間;例如,America/New_York 時區的偏移量在夏令時為 -4:00,在其他時間為 -5:00。因此,當必 須考慮特定於區域設置的時間規則(如夏令時)時,應該使用 ZonedDateTime 類。
ZonedDateTime 類提供了幾種類別的用於創建、訪問和修改瞬間的方法。要創建 ZonedDateTime 的新 實例以表示計算機的默認時區中的當前系統時間,可以使用 Clock.currentZonedDateTime() 工廠方法, 如下面的示例所示。
Clock systemClock = Clock.system();
ZonedDateTime currentTime = systemClock.currentZonedDateTime();
要創建一個 ZonedDateTime 實例以表示特定的、預先確定的日期,可以使用多個 ZonedDateTime.dateTime 工廠方法之一。下面的示例演示了如何創建一個 ZonedDateTime 以表示計算機 的默認時區中的 2000 年 1 月 1 日午夜。
Clock systemClock = Clock.system();
TimeZone defaultTimeZone = systemClock.timeZone();
int year = 2000;
int month = 1;
int day = 1;
int hour = 0;
int minute = 0;
ZonedDateTime theDate = ZonedDateTime.dateTime(year,
month, day, hour, minute, defaultTimeZone);
OffsetDateTime 實例的創建方式 與 ZonedDateTime 實例類似,區別在於傳入 OffsetDateTime.dateTime 工廠方法的是 ZoneOffset 而不 是 TimeZone。要獲得 ZoneOffset 的實例,可以使用靜態 ZoneOffset.zoneOffset(int hours) 方法, 其中 hours 是與 UTC 之間的偏移量(以小時為單位)。
不完全時間
不完全時間是這樣一種日期或時間表示,即它不足以指定時間線上的特定唯一點。例如,“6 月 7 日 ”是一個不完全時間,因為它指定月份和日期,但未指定年份或每日時間。因此,上述不完全時間不足以 標識時間線上的唯一點。因為不完全時間不標識特定的時間,所以無法將其表示為與標准紀元之間的偏移 量(以納秒為單位)。它們的定義必然是基於字段的,並且使用年、月、日和每日時間等日歷字段。
可以根據不完全時間所定義的日歷字段集對其進行分類。例如,表示年度假期的不完全時間可能包含 月和日字段,而表示商店開業時間的不完全時間可能包含時和分字段。JSR 310 為常用的不完全時間提供 了類,例如 MonthDay(前面提到的“假期”不完全時間)、LocalDate(不帶時間或時區的日期)以及 LocalTime(不帶時區的時間)。要創建 MonthDay 或其他某個現成的不完全時間類的實例,可以使用所 提供的靜態工廠方法之一。下面的示例特定於 MonthDay,但可以輕松地針對其他某個不完全時間類改寫 該示例。
//Create a MonthDay representing December 25 (Christmas)
int theMonth = 12;
int theDay = 25;
MonthDay christmas = MonthDay.monthDay(theMonth, theDay);
持續時間
持續時間表示一段已經流逝的時間,並且定義到納秒級;例如“100,000 納秒”是一個持續時間。持 續時間有點類似於時段,後者也定義了一段已經流逝的時間;但是,與時段不同,持續時間表示已流逝納 秒的精確數目。
可以將持續時間添加到瞬間以返回一個新的瞬間,該瞬間與原始瞬間之間偏移持續時間中的納秒數。 例如,如果持續時間“86,400,000,000,000 納秒”(24 小時)被添加到瞬間“世界標准時間 2008 年 3 月 1 日午夜”,則產生的瞬間為“世界標准時間 2008 年 3 月 2 日午夜”。
可以用以下兩種方式創建持續時間:通過為持續時間提供秒、毫秒或納秒時間跨度,或者通過提供起 始瞬間和結束瞬間。第一種方法適合於創建具有預先確定的長度的持續時間:
System.out.println("Enter a duration length in hours:");
Scanner s = new Scanner(System.in);
int durationSeconds = 3600 * s.nextInt();
Duration d = Duration.duration(durationSeconds);
作為一種替代方式,您可能希望 確定兩個預先確定的瞬間之間的持續時間。在這種情況下,靜態的 Duration.durationBetween (ReadableInstant startInclusive, ReadableInstant endExclusive) 工廠方法很有用。
Clock systemClock = Clock.system();
ZonedDateTime instant1 = systemClock.currentZonedDateTime();
try{Thread.sleep(1000)} //Use up some time
catch(InterruptedException e){System.out.println("Error")}
ZonedDateTime instant2 = systemClock.currentZonedDateTime();
Duration d = Duration.durationBetween(instant1, instant2);
時段
像持續時間一樣,時段表示一段已經消逝的時間。時段的示例有“4 年零 8 天”和“1 小時”。如這 些示例所示,時段是使用日歷字段(年、日、小時等)定義的,而不是通過確切的納秒數確定的。
首先,時段和持續時間可能看起來像是表達同一概念的不同方式;但是,正如下面的瞬間/時段加法示 例所演示的那樣,無法將給定的時段轉換為確切的納秒數。瞬間/時段加法將時段的每個日歷字段的值添 加到瞬間的相應字段。瞬間/持續時間加法與此相反,該加法將持續時間的長度(以納秒為單位)添加到 瞬間。乍看起來,向給定的瞬間添加 86,400,000,000,000 納秒(24 小時)持續時間似乎應該與添加“1 天”的時段產生相同的結果,但情況並非總是如此,原因是日歷字段“日”不具有固定的納秒長度。大多 數日都是 24 小時長,但由於存在夏令時,因此某些日要長一些或短一些。向瞬間添加 24 小時持續時間 總是將該瞬間恰好推進 24 小時,而添加一個 1 天的時段總是將日推進 1,同時使每日時間保持不變。
例如,如果將時段“1 日”添加到瞬間“美國東部時間 2008 年 3 月 9 日午夜”,則得到的基於字 段的加法將產生“美國東部時間 2008 年 3 月 10 日午夜”。但是,如果將一個 24 小時的持續時間添 加到瞬間“美國東部時間 2008 年 3 月 9 日午夜”,則結果為“美國東部時間 2008 年 3 月 10 日 01:00:00.0”。兩者之間的差別源於:夏令時開始於美國東部時間 2008 年 3 月 9 日 02:00:00;因此 ,該日只有 23 小時長。
JSR 310 通過多個類提供了時段功能,其中最為重要的類是 Period、Periods.Builder 以及 Periods.Builder 的子類:Periods.SecondsBuilder、Periods.MinutesBuilder(它是 SecondsBuilder 的子類)等(一直到 Periods.YearsBuilder)。
要創建 Period 的實例,您必須首先使用靜態 Periods.periodBuilder() 方法獲取 Periods.Builder 的實例。此方法返回一個 Periods.YearsBuilder 對象,這是 Periods.Builder 的一個間接子類。 Periods.YearsBuilder 類提供了一個方法 years(int numYears),該方法將 numYears 年添加到所生成 的時段。使用 YearsBuilder 的直接和間接超類(MonthsBuilder、DaysBuilder、HoursBuilder、 MinutesBuilder 和 SecondsBuilder)可以將其他日歷字段添加到該時段。例如, MinutesBuilder.minutes(int numMinutes) 向所生成的時段添加 numMinutes 分鐘。上述所有方法都返 回 this,因而,只需一條語句就可以生成具有多個日歷字段的時段。請注意,通過將一個負整數傳遞到 適當的 builder 方法中,可以對時段執行減法。在將所有需要的字段添加到該時段之後,將調用 Periods.PeriodBuilder.build() 方法以返回 Period 的完整實例。
//Build a period representing "8 years, 3 months."
Period thePeriod =
Periods.periodBuilder().years(8).months(3).build();
時間間隔
時間間隔表示時間線上的兩個瞬間之間的一段時間。因此,“世界標准時間 2007 年 1 月 1 日(包 括)到世界標准時間 2008 年 1 月 1 日(不包括)”是一個時間間隔。時間間隔可以包括或不包括起始 瞬間和結束瞬間。
JSR 310 提供了 InstantInterval 類來表示時間間隔。要創建 InstantInterval 的實例,可以使用 多個工廠方法之一:
//Create some intervals.
ZonedDateTime time1 = systemClock.currentZonedDateTime();
try{Thread.sleep(1000)} //Use up some time
catch(InterruptedException e){System.out.println("Error")}
ZonedDateTime time2 = systemClock.currentZonedDateTime();
//Create an interval with inclusive start and exclusive end.
InstantInterval interval1 = InstantInterval.intervalBetween(
time1.toInstant(), time2.toInstant());
//Create an interval with exclusive start and inclusive end.
boolean startInclusive = false; boolean endInclusive = true;
InstantInterval interval2 = InstantInterval.intervalBetween(
time1.toInstant(), startInclusive,
time2.toInstant(), endInclusive);
使用 JSR 310 日期/時間類
前面的部分介紹了 5 個基本 JSR 310 日期/時間概念,並且說明如何實例化表示這些概念的類。本部 分演示一些可以使用 JSR 310 API 進行的日歷計算。
對了進行演示,假設您正在開發的程序采用多個在不同時間發生並且按照特定時間間隔重復發生的事 件作為輸入。該程序輸出所有同時發生兩個或更多個事件的時間。您可以使用一個 ZonedDateTime 和一 個 Duration 來表示在經過固定長度的時間之後重新發生的瞬間事件;要生成該事件的下一個實例,可以 向表示上一個實例的 ZonedDateTime 添加 Duration:
//ZonedDateTime firstTime is the time the event first occurs
//Duration repeatTime is the duration after which event recurs
//TimeZone defaultZone is your computer's default time zone
ZonedDateTime secondTime = ZonedDateTime.dateTime(
firstTime.toInstant().plus(repeatTime), defaultZone);
如果您具有一個在特定 數量的年、月或日之後重復發生的事件,則可以通過向表示上一個實例的 ZonedDateTime 添加一個 Period 來生成下一個實例:
//ZonedDateTime firstTime is the time the event first occurs
//Period repeatPeriod is the period after which event recurs
ZonedDateTime secondTime = firstTime.plus(repeatPeriod);
要測試兩個瞬間事件是 否同時發生,可以使用 ZonedDateTime.equals(Object other) 方法:
//time1 and time2 are ZonedDateTimes representing events
if (time1.equals(time2)){
System.out.println("The two events occur simultaneously.");
}
如果您需要表示一個非瞬間事件,則使用 ZonedDateTime 並非最佳選擇。不完全時間 更加適合於表示非瞬間事件(如年假)。要確定一個由 ZonedDateTime 表示的事件和一個由不完全時間 (例如,MonthDay)表示的事件是否同時發生,可以使用 MonthDay.matchesDate(LocalDate date) 方法 。如果 LocalDate(它是一個不帶每日時間的日期)中的月和日字段與 MonthDay 中的月和日字段匹配, 則此方法返回 true。
//Monthday christmas is a partial representing December 25.
//ZonedDateTime time1 represents an instantaneous event
if (christmas.matchesDate(time1.localDate())){
System.out.println("The event occurs on Christmas.");
}
最後,通過 InstantInterval 可以最佳地表示按照特定時間跨度發生的事件。要測試 一個 InstantInterval 是否與一個 ZonedDateTime 重疊,可以使用 InstantInterval.contains (InstantProvider instant) 方法。(InstantProvider 是一個由三個 JSR 310 瞬間類實現的接口。)
//InstantInterval event1 is an event that occurs over a timespan
//ZonedDateTime event2 is an instantaneous event
if (event1.contains(event2)){
System.out.println("Event 2 occurs during event 1.");
}
結束語
本文演示了 JSR 310 的一些功能,JSR 310 是為 Java 7 提議的一個新的 Java 日期/時間 API。具 體說來,本文演示了表示下列 5 個 JSR 310 日期/時間概念的類的用法:瞬間、不完全時間、持續時間 、時段和時間間隔。JSR 310 中還有其他許多功能未在本文中予以介紹,如高級日期/時間匹配功能。此 外,因為本文主要基於當前 JSR 310 API 的快照,所以在本文發布之後可能添加其他功能。有關 JSR 310 的功能的更多信息,請參見參考資料。