從JDK時間API演進,看時間處理


iThome 網站首載:從JDK時間API演進,看時間處理

各語言平台都會提供時間處理API,視程式需求而定,它可能是個不起眼的API,然而如果經常處理時間,就會知道時間處理包含許多複雜因素。JDK目前在java.util套件提供DateCalendar來處理時間,為了應付複雜的時間處理,JDK8納入了規範新時間處理API的JSR310。觀察從DateCalendar到JSR310的演進,我們可瞭解到時間處理有哪些基本要素需要考量。

幾個需要知道的時間概念

多數程式語言或電腦系統計算時間的基準,是以epoch,也就是西元1970年1月1日0時起算的秒數或毫秒數,時間字串表示通常會顯示GMT字樣,GMT是指「格林威治標準時間」(Greenwich Mean Time),它是以觀察地球自轉而得,並以格林威治天文台為時間原點。實際上,epoch是使用UTC(Coordinated Universal Time),又稱為「世界統一時間」,也就是原子鐘的西元1970年1月1日0時。因為地球自轉會受到其它天體、自身潮汐等影響而出現偏差,為了讓UTC符合GMT,每隔一段時間UTC必須插入閏秒(Leap second),讓UTC與GMT保持在0.9秒之內的誤差,由於差別細微,多數情況下UTC與GMT意義相同,不過有些API會採用或區別不同名稱。

世界各地是以格林威治天文台時間為基準,然而每個國家或地區的日出時間有所不同,因此必須有所調整,稱之為「時差」,通常是以小時為單位。以台灣而言,位於格林威治天文台東邊,日出時間會快八個小時,因此時間表示會顯示「GMT +0800」。日出時間並非國家或地區制訂統一時間的標準,例如美國國土廣大,地區各自以日出時間為準反而會有所困擾,因而僅制定六個「時區」(Time zone),而中國現今只有一個時區,以北京時間為標準。除了時差與時區的考量之外,有些國家或地區還實施日光節約時間(Daylight saving time, DST),又稱為夏令時間,這是為了利用夏日較長的日照時間,達到節約能源的考量,各國實行規定並不相同,以美國為例,自2007年起,3月的第二個星期至11月的第一個星期日將時間提前一小時。

目前多數國家或地區採用的歷法為格里高利曆法(Gregorian calendar),為改革歐洲過去使用的儒略曆法(Julian calendar)而來,由教宗格列高利十三世在1582年頒行,改曆時將儒略曆1582年10月4日星期四的次日,訂為格里曆1582年10月15日星期五,每個國家改曆的時間不一,美國與英國都是在格里曆1752年9月14日星期四(也就是儒略曆1752年9月2日星期三的次日)改曆,Unix/Linux的cal指令支援改曆,因此在Unix/Linux中執行cal 1752,可看到9月的憑空少了11天。

被廢棄的java.util.Date

Date類別實例反映的是UTC,建構Date實例時給予的long整數,代表著從epoch開始歷經的毫秒數,如果沒有給定就是依賴於JVM時間(透過System.currentTimeMillis()取得)。在JDK1.1前,Date還允許指定年、月、日、時、分、秒,也允許從字串剖析而得Date實例,或者是將Date實例格式化為字串表示,然而這兩個功能的建構式或相關方法,並不利於國際化(Internationalization),像是無法調整時區設定、自訂格式或進行本地化(Localization)等。被廢棄的相關方法之職責,轉由java.util.Calendarjava.text.DateFormat取代。

Date類別上大多數建構式與方法被廢棄,並不代表Date類別被廢棄。Date類別保留了Date()以及Date(long date)建構式。除了重新定義Object類別的五個方法之外,未被廢棄的方法僅留getTime()setTime(long time)。這表示Date實例最後留下的職責就是作為值物件(Value object),用以代表某個瞬時(Instant)狀態,時間的年、月、日等訊息,為必要時才計算出來,這樣的物件容易傳遞,也是不少方法在介面上仍採用Date類別的原因之一。

然而可以看出,Date除了代表某個瞬時狀態外,並沒有計算其它時間資訊的能力,像是計算兩個Date的時間差、某個Date經過七天之後的時間等,單是使用Date來運算的話,開發者必須親自撰寫演算法,若對時間的需求涉及時差、時區或甚至日光節約時間時,程式將會更複雜。

接替職責的java.util.Calendar

JDK1.1發佈時,接替Date被廢棄方法職責的為java.util.Calendarjava.text.DateFormat。從Calendar的建構式Calendar(TimeZone zone, Locale aLocale)就可看出,它支援時區與本地化,並可以進行時間資訊計算,像是某設定時間歷經若干年、月、日後的時間,與另一時間物件進行比較等。Calendar實際上是個抽象類別,JDK提供的標準實作是java.util.GregorianCalendar,從名稱上可知實作了格里高利曆法,可透過工廠方法Calendar.getInstance()取得,預設改歷時間為1582年10月15日,可藉由setGregorianChange()方法指定。

GregorianCalendar需要時區資訊,以TimeZone類別代表,藉由時區資訊可得知當地實際時間,並支援如日光節約時間之運算,例如若時區設定為Asia/Taipei,而Calendar實例設定為1975年3月31日0時,使用add方法增加1日,會發現Calendar實例的時間是1975年4月1日1時,平白多了一個小時,這是因為台灣於某些時期也曾經實行過日光節約時間。

Calendar提供了時間計算的彈性,然而使用上過於複雜,如果只是需要電腦上的本地時間,不在乎時差或時區等相關資訊,或者想加上某個時段並格式化輸出,還得搭配DateFormatLocale等寫上冗長的程式。
   
依時間概念分工的Joda-time與JSR310

使用Date來代表瞬時的問題,在於它的狀態是可變動的(Muttable),也就是說,若有個類別在建構實例時接受Date實例,即使該類別沒有提供其它方法來修改封裝的Date實例,依舊可透過DatesetTime()方法來修改Date狀態。類似地,Calendar也是可變動的,並有使用時API過於複雜問題,這使得開發者尋求其他時間API選擇。2005年Stephen Colebourne開發了Joda-Time,用以替代JDK的DateCalendar,Stephen後來向JCP提交了JSR310,並成為該規格領導人,JSR310原本預計包括在JDK7,後來延遲至JDK8中兌現。

JSR310主要基於Joda-Time API,相關API座落在java.time套件,重要概念是基於不連續時間線(Discrete timeline),時間線上特定點為瞬間,由Instant類別代表,大致對應於Date,也就是相對於epoch的偏移量,不過支援至奈秒;時間線上的兩個瞬間的差為持續期間,由Duration類別定義,可精確至奈秒等級,如果有段程式需要34.5秒這樣的持續時間,就可以使用Duration實例作為代表;有些時間資訊是不完全的(Partial),例如5月26日就是不完全的時間資訊,代表任何年中的5月26日,可以使用MonthDay實例代表。容易與持續期間混淆的概念之一是時段,使用Period類別代表,基於日曆的年、月、日概念制訂,與持續期間區別的方式之一,就是思考日曆上不會有奈秒這樣的概念,因而像是兩天、三個月或四年,就可使用Period實例代表。

許多程式需要的時間概念並不需要時區或UTC偏移量,Calendar強迫你要接受或者自行抽取資訊,JSR310則將時區與UTC偏移量區隔出來,像是LocalDateTimeLocalDateLocalTime是不帶有時區及UTC偏移量資訊的時間代表,OffsetDateTimeOffsetTime是加入UTC偏移量的代表,而ZonedDateTime則加入了時區規則,以上提及的JSR310等類別都設計為不可變動(Immutable),避免了上述DateCalendar的問題。

從以上可看出,時間處理本身是很複雜的議題,事實上若要整個瞭解JSR310,也會是個龐大的工作,然而實際上應需求而定,你需要的也許僅是時間處理議題中某個部份。Joda-time或JSR310主要是將各種常用時間概念加以區分,如果你只是需要某個時間概念的表現或運算,只需要取用特定部份的API,從而化解了處理時間的繁瑣。JSR310也提供了DateCalendar的銜接,讓開發者只需修改時間計算部份,而不需修改既有程式介面。作為JSR310參考實作,ThreeTen原始碼可在Github取得,目前已整合至OpenJDK中。