?
在計算機程序中精確的處理日期是困難的。不僅有顯而易見的(英語:?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基類提供的附加功能分成三類.?幾個靜態的工廠方法來獲得用任意時區和locales初始化的實例.?如前面提到的,?所有以這種方式獲得實例已經被初始化為當前時間.?沒有工廠方法被提供來獲得初始化為任意時間點的實例.
第二組包含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?定義了嚴格的建立自定義時區標示符的語法.?也提供了靜態工廠方法用以獲取?—?指定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?沒有定義靜態方法來完成文本和日期之間的格式化和轉換.?作為替代,?它定義了幾個靜態工廠方法來獲取被初始化為給定locale和選定style的實例.?既然標準的格式化總是包含日期和時間,?附加工廠方法可用來獲取僅處理時間或日期部分的實例.?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()方法.?此外,?每個類提供了靜態的工廠方法,?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.?希望這篇文章對幫助你搞清它們的用法有所幫助.