簡介
在電腦程式中精確的處理日期是困難的。不僅有顯而易見的(英語: January, 法語: Janvier, 德語: Januar, 等)國際化需求, 而且得考慮不同的日期系統(並非所有的文化都用基督耶穌的生日作為紀年的開始)。如有高精度或非常大規模的時間需要被處理, 就有額外的方面需要被注意,比如閏秒或時間系統的變化。(西曆(陽曆, 格里高利曆法)在西方被普遍接受是在1582年,但並非所有的國家在同一天接受!)
儘管有關於閏秒, 時區, 夏令時, 陰曆的問題, 度量時間卻是一個非常簡單的概念: 時間的進行是線性很容易被忽略。一旦時間軸的地區被定義, 任何時間點被從起點時間的流逝就可以確定。這和地理位置或當地時區是獨立的 – 對任意指定的時間點, 對任意地區, 從起點的過程是相同的(忽略相對論的矯正)。
--------------------------------------------------------------------------------
可當我們試圖根據某些日曆解釋這一時間點的時候困難來了, 比如, 根據月, 日, 或者年來表示它。在這一步上地理資訊變得相關: 在時間上的同一個點對應不同的天的某一時間, 依賴於地區 (比如: 時區)。基於解釋日期的修正經常是必要的(今天一個月以後是哪一天?) 並且增加了額外的困難: 上溢和下溢(12月15號的後一個月是下一年), 且不明確(1月30號後的一個月是哪一天?).
在最初的JDK 1.0, 一個時間點, 通過把它解釋為java.util.Date類, 它被計算在一起來表示. 雖然相對容易處理, 但它並不支援國際化; 從JDK 1.1.4 或JDK 1.1.5, 多樣的負責處理日期的職責被分配到以下類中:
java.util.Date
代表一個時間點.
abstract java.util.Calendar
java.util.GregorianCalendar extends java.util.Calendar
解釋和處理Date.
abstract java.util.TimeZone
java.util.SimpleTimeZone extends java.util.TimeZone
代表一個任意的從格林威治的位移量, 也包含了適用於夏令時(daylight savings rules)的資訊.
abstract java.text.DateFormat extends java.text.Format
java.text.SimpleDateFormat extends java.text.DateFormat
變形到格式良好的, 可列印的String, 反之亦然.
java.text.DateFormatSymbols
月份, 星期等的翻譯, 作為從Locale取得資訊的一種替代選擇.
java.sql.Date extends java.util.Date
java.sql.Time extends java.util.Date
java.sql.Timestamp extends java.util.Date
代表時間點, 同時為了在sql語句中使用也包含適當的格式.
注意: DateFormat 和相關的類在java.text.*包. 所有的java.sql.*包中日期處理相關類繼承了java.util.Date類. 所有的其它類在java.util.*包中.
這些"新"類來自三個分離的繼承層次, 其頂層類(Calendar, TimeZone, and DateFormat)是抽象的. 針對每一個抽象類別, Java標準類庫提供了一個具體的實現.
java.util.Date
類java.util.Date代表一個時間點. 在許多應用中, 此種抽象被稱為"TimeStamp." 在標準的Java類庫實現中, 這個時間點代表Unix紀元January 1, 1970, 00:00:00 GMT開始的毫秒數. 因而概念上來說, 這個類是long的簡單封裝.
根據此種解釋, 類中僅有的沒有到期的(除了那些毫秒數的get和set方法)是那些排序方法.
這個類依靠System.currentTimeMillis() 來取得當前的時間點. 因此它的準確度和精度由System的實現和它所調用底層(本質是作業系統)決定.
The java.util.Date API
在最初的 Date類使用中名字和約定引起了無盡的混淆. 然而用0-11計算月, 從1900計算年的決定模仿了C標準類庫的習慣, 調用函數 getTime()返回起始於Unix紀元的毫秒數和 getDate()返回星期的決定顯然是Java類設計者自己的.
java.util.Calendar
語義
Calendar代表一個時間點(一個"Date"), 用以在特定的地區和時區適當的解譯器. 每一個Calendar 執行個體有一個包含了自紀元開始的代表時間點的long變數.
這意味著Calendar 不是一個(無狀態) 變換者或解譯器, 也不是一個修改dates的工廠. 它不支援如下方式:
Month Interpreter.getMonth(inputDate) or
Date Factory.addMonth(inputDate)
Instead, Calendar執行個體必須被初始化到特定的Date. 此Calendar執行個體可以被修改或查詢interpreted屬性.
奇怪的是, 此類的instances 總是被初始化為目前時間. 獲得一個初始化為任意Date的Calendar 執行個體是不可能的—API強製程序員通過一系列的在執行個體上的方法調用, 比如setTime(date)來顯式的設定日期.
訪問Interpreted 欄位和類常量
Calendar類遵從一不常用的方式來訪問interpreted date執行個體的單個欄位. 而不是提供一些dedicated屬性 getters和setters方法(比如getMonth()), 它僅提供了一個, 使用一個標示作為參數來擷取請求的屬性的方法:
int get(Calendar.MONTH) 等等.
注意這個函數總是返回一個int!
這些欄位的標示符被定義為Calendar類的public static final變數. (這些identifiers是raw的整數, 沒有被封裝為一個枚舉抽象.)
除了對應這些欄位標示(索引值), Calendar 類定義了許多附加的public static final 變數來儲存這些欄位的值. 因此, 為測試某一特定date (由Calendar 的執行個體calendar表示) 是否在一年的第一個月, 會有像如下的代碼:
if (calendar.get(Calendar.MONTH) == Calendar.JANUARY) {...}
注意月份被叫做 JANUARY, FEBRUARY, 等等, 不管location(相對更中性的名字比如: MONTH_1, MONTH_2, 等等). 也有一個欄位UNDECIMBER, 被一些(非西曆(陽曆, 格里高利曆法))日曆使用, 代表一年的第十三個月.
不幸的是, 索引值和值既沒有通過名字也沒分組成嵌套的inerface來區分.
處理
Calendar提供了三種辦法來修改當前執行個體代表的日期: set(), add(), 和roll(). set()方法簡單的設定特定的欄位為期望的值. add() 和 roll() 的不同在於它們處理over- and underflows: add() 傳遞變更到"較小"或"較大"的欄位, 而roll()不影響其它欄位. 比如, 當給代表12月15號的Calendar執行個體增加一個月, 當add()使用年會增加, 但使用roll()不會發生任何變化. 為每一種case對應一個函數的決定的動機是, 它們可能在GUI中不同的使用情形.
由於Calendar的實現的方式, 它包含冗餘的資料: 所有的欄位都可以從給定的時區和紀元開始的毫秒數計算出來,反之亦然. 這個類為這些操作分別定義了抽象方法computeFields()和computeTime(), 又定義了complete()方法執行完全的來回旅程. 因為有兩套冗餘的資料, 這兩套資料可能不同步. 根據類的JavaDoc文檔, 當發生變更的時候依賴的資料以lazily 的方式重新計算. 當重新計算需要的時候, 子類必須維護一套髒資料標誌作為符號.
--------------------------------------------------------------------------------
實現的Leakage
對於一個”新”的日期相關處理類, 不得不說實現的細節在某種程度上被泄漏到了API中. 在這點上, 這是它們有意用作基類的自訂開發的反映, 但它也偶然看出是不充分清晰設計一個公用介面的結果.Calendar 抽象是否維護兩個冗餘資料集合完全是一個實現的細節, 因而應當對客戶類隱藏. 這也包括打算通過繼承來重用此類.
附加的功能
Calendar基類提供的附加功能分成三類. 幾個靜態Factory 方法來獲得用任意時區和locales初始化的執行個體. 如前面提到的, 所有以這種方式獲得執行個體已經被初始化為目前時間. 沒有Factory 方法被提供來獲得初始化為任意時間點的執行個體.
第二組包含before(Object)和after(Object)方法. 它們接受Object類型的參數, 因而允許這些方法被子類以任意類型的參數覆蓋掉.
最後, 有許多附加的方法來獲得設定附加的屬性, 比如當前的時區. 當中有幾個用以查詢特定欄位在當前Calendar實現下的可能和實際的最大、最小值.
java.util.GregorianCalendar
GregorianCalendar 是僅有可用的Calendar的子類. 它提供了基礎Calendar抽象適合於根據在西方的習慣解釋日期的實現. 它增加了許多public的建構函式, 也有針對於Gregorian Calendars的方法, 比如isLeapYear().
java.util.TimeZone 和 java.util.SimpleTimeZone
TimeZone類和其子類是輔助類, 被Calendar用以根據選擇的時區來解釋日期. 按字面意思來說, 一個時區表示加到GMT上後到當前時區的一定的位移. 顯然, 這個位移在夏令時有效時候會發生變化. 因而為了計算對於給定日期和時間的本地時間, TimeZone抽象不僅需要明白當DST有效時的額外位移, 而且還需明白什麼時候DST有效規則.
抽象基類TimeZone 提供了基本的處理"raw"(沒有考慮夏令時)實際位移(用毫秒數!)的方法, 但任何關於DST規則的功能實現被留給了子類, 比如SimpleTimeZone. 後者提供了許多方法來指定控制DST開始和結束的規則, 比如在一個月中明確的某一天或某一天隨後的周幾. 每一個TimeZone 有一個可讀的, 本地無關的顯示名. 顯示名以兩種風格: LONG和SHORT呈現.
星期的開始?
Calendar的文檔投入了相當的文字來正確的計算月或年中的weeks. weekday 被認為是一周的開始在因國家的不同而不同. 在美國, 一周通常被認為從周日開始. 在部分歐洲國家一周從周一開始結束於周日.這將影響到哪一周被認為是在一年(或月)第一個完整的周, 和計算一年的周數.
時區由一標示字串明確的決定. 基類提供靜態方法String[] getAvailableIDs()來獲得所有已知安裝(JDK內帶有)的標準時區. (在我的安裝內有557個, JDK1.4.1) 假如需要, JavaDoc 定義了嚴格的建立自訂時區標示符的文法. 也提供了靜態Factory 方法用以擷取 — 指定ID或預設的當前時區的TimeZone 執行個體. SimpleTimeZone提供了一些公有的建構函式, 奇怪的是對於一個抽象類別, 如TimeZone. (JavaDoc 寫到 "子類建構函式調用." 顯然, 應該聲明為protected.)
java.text.DateFormat
儘管Calendar和相關類處理locale-specific日期的解釋,仍有DateFormat 類輔助日期和(人類)可閱讀字串之間的變換. 表示一個時間點時, 會出現附加的本地化問題: 不僅僅在語言, 而且日期格式是地區獨立的(美國: Month/Day/Year,德國: Day.Month.Year, 等等). DateFormat 儘力地為程式員管理這些不同.
抽象基類DateFormat不需要(且不允許) 任意的, 程式員定義的日期格式. 作為替代, 它定義了四種格式化風格: SHORT, MEDIUM, LONG, 和FULL (以冗餘增加的順序).對一給定locale和style, 程式員可依靠此類擷取適當的日期格式.
抽象基類DateFormat 沒有定義靜態方法來完成文本和日期之間的格式化和轉換. 作為替代, 它定義了幾個靜態Factory 方法來擷取被初始化為給定locale和選定style的執行個體. 既然標準的格式化總是包含日期和時間, 附加Factory 方法可用來擷取僅處理時間或日期部分的執行個體. String format(Date)和Date parse(String) 方法然後執行變形. 注意具體的子類可以選擇打破這種習慣.
在其內部使用, 解釋日期的Calendar對象是可訪問和修改的, TimeZone和NumberFormat對象也同樣. 然而, 一旦DateFormat 被執行個體化locale和style就不能再修改.
亦有可用的(抽象的)用以拼接的字串解析和格式化的方法, 分別接受額外的ParsePosition或FieldPosition參數. 這些方法的每一個都有兩個版本. 一個接受或返回Date執行個體另一個接受或返回普通的Object, 來允許在子類中有選擇性的處理Date. 它定義了一些以_FIELD 結尾的public static變數來標示多種可能和FieldPosition一起使用的變數(cf. java.util.Format的Javadoc).
僅有且最常用的DateFormat類的具體實現是SimpleDateFormat. 它提供了所有上述的功能, 且允許定義任意的時間格式. 有一套豐富文法來指定格式化模式; JavaDoc提供了所有細節. 模式可以被指定為建構函式的參數或顯式的設定.
Printing a Timestamp: A Cut-and-Paste Example
想象你要用使用者定義的格式列印當前的時間; 比如, 到log檔案. 以下就是做這些的:
// 建立以下格式的模式: Hour(0-23):Minute:Second
SimpleDateFormat formatter = new SimpleDateFormat( "HH:mm:ss" );
Date now = new Date();
String logEntry = formatter.format(now);
// 從後端讀入
try {
Date sometime = formatter.parse(logEntry);
} catch ( ParseException exc ) {
exc.printStackTrace();
}
注意需要被catch的ParseException. 當輸入的字串不能被parse的時候被拋出.
java.sql.*相關類
在java.sql.*包中的日期時間處理類都繼承了java.util.Date. 事實上它們三個反映了三種標準SQL92模型的類型需要DATE, TIME, and TIMESTAMP.
像java.util.Date, SQL包中的這三個類是表示一個時間點的數位簡單封裝. 分別地Date和Time類忽略關於一天中的時間或日曆的日期.
可Timestamp類, 不僅包含到毫秒精度, 通常的時間和日期, 而且允許儲存附加的精確到納秒精度的時間點的資料. (納秒是一秒的十億分之一)
除了影射對應的SQL資料類型, 這些類處理與SQL一致的字串表示的轉換. 在這一點, 這三個類中的每一個覆蓋了toString()方法. 此外, 每個類提供了靜態Factory 方法, valueOf(String), 返回被初始化為傳遞參數字串表示的時間的當前調用類的執行個體. 這三個方法的字串表示的格式已被SQL標準選定, 且不能被程式員改變.
儲存納秒需要的額外資料, 沒有很好的與在Timestamp中其它通常的時間和日期資訊的表示一致. 比如, 在Timestamp執行個體上調用 getTime() 將返回自Unix紀元開始的毫秒數,忽略了納秒資料. 簡單地, 根據JavaDoc文檔, hashCode() 方法在子類中沒有被覆蓋, 因而也忽略了納秒資料.
java.sql.Timestamp的JavaDoc指出"inheritance relationship (...) 實際表示實現的繼承, 而不是類型繼承(這違反了繼承的初衷). 但即使這句話是錯誤的, 既然Java沒有私人繼承的概念(也即繼承實現). 所有java.sql.*包中的類應該被設計為封裝一個java.util.Date對象, 而不是繼承它, 僅暴露需要的方法 — 最起碼, 方法比如hashCode() 應該被適當的覆蓋.
最後一個評論是關於資料庫引擎的時區的處理. 在java.sql.*包中的類不允許顯式的設定時區. 資料庫伺服器(或驅動) 可自由的依據伺服器server的當地時區解釋這些資訊, 且其可能被影響而變化(比如, 因為夏令時).
總結
通過前面的討論, 很清楚, Java的日期處理相關類並非很複雜, 但是沒有被很好設計. 封裝被疏漏, APIs結構複雜且沒有被很好的組織, 且非常見的思路經常被無緣由的使用. 實現更有其它的莫名奇妙(提議看看Calendar.getInstance(Locale)對於所有可用locale實際返回對象的類型!) 另一方面, the classes manage to treat all of the difficulties inherent in internationalized date handling and, in any case, are here to stay. 希望這篇文章對協助你搞清它們的用法有所協助.
Call Me By My True Names
As a last example of the wonderful consistency and orthogonality of Java's APIs, I would like to list three (maybe there are more!) different methods to obtain the number of milliseconds since the start of the Unix epoch:
long java.util.Date.getTime()
long java.util.Calendar.getTimeInMillis() (New with JDK 1.4.1. Note that java.util.Calendar.getTime() returns a Date object!)
long java.lang.System.currentTimeMillis()
感謝
作者非常感謝Wilhelm Fitzpatrick (西雅圖) 細心的閱讀原稿和有價值的意見.
References
International Calendars in Java at IBM : A detailed white paper by one of the original authors on the genesis and intended usage of Java's date-handling classes. Highly recommended.
IBM alphaWorks: International Calendars: Additional subclasses of Calendar for Buddhist, Hebrew, Muslim, and Japanese calendars used to be available at IBM's alphaWorks. Unfortunately, they seem to be temporarily unavailable.
Reingold on Calendars: Web site of Edward M. Reingold, author of Calendrical Calculations, the standard reference on calendars.
About the Calendars: A brief overview of some of the more common international calendars.
Thread on JavaLobby: A brief, but interesting, thread on JavaLobby. Apparently, some people considered the APIs of Java's date classes to be so bad that they filed an official bug-report to have them changed. Unfortunately, the request has been rejected.
Philipp K. Janert, Ph.D. is a Software Project Consultant, server programmer, and architect.