【Joda-Time 與 JSR310 】系列,將會以我在 2013 Java TWO 分享的 Joda-Time & JSR 310 內容為基礎。
日期與時間處理 API,在各種語言中,可能都只是個不起眼的 API,如果你沒有較複雜的時間處理需求,可能只是利用日期與時間處理 API 取得系統時間,簡單地做些顯示罷了,然而如果真的要認真看待日期與時間,其複雜程度可能會遠超過你的想像,天文、地理、歷史、政治、文化等因素,都會影響到你對時間的處理。
在 Java 的世界中,處理日期最基本的 API 就是 java.util.Date
,這個遠從 JDK 1.0 就已存在的古老 API,如果你只是要取得日期時間作些顯示,也許還沒有問題,如果你打算以它作為基礎進行日期時間,那就很可能落人陷阱,來舉個實際遇過的例子:
import java.util.*;
import java.text.*;
public class Main {
public static void main(String[] args) throws Exception {
DateFormat dateFormat =
new SimpleDateFormat("yyyy-MM-dd");
Date birthDate = dateFormat.parse(args[0]);
Date nowDate = new Date();
long lifeMillis = nowDate.getTime() - birthDate.getTime();
System.out.printf("你今年的歲數為:%d%n",
lifeMillis / (365 * 24 * 60 * 60 * 1000));
}
}
看來沒什麼問題?從命令列引數取得日期格式字串並剖析為 Date
實例,取得 1970 年 1 月 1 日起始至今的毫秒數進行相減,就是目前你目前活了毫秒數,真的嗎?一年的毫秒數真的是 (365 * 24 * 60 * 60 * 1000)
嗎?別的不說,我輸入 "1975-05-26"
,算出來可是 800 多歲呢!… 原因?(365 * 24 * 60 * 60 * 1000)
溢位(Overflow)了 … 詳細原因可以參考 Promotion 與 Cast,如果單是解決溢位問題,就在分母加個 L
就可以了:
System.out.printf("你今年的歲數為:%d%n",
lifeMillis / (365 * 24 * 60 * 60 * 1000L));
其實在建構 Date
實例時也有些陷阱,例如,你想表示 "2013-08-02"
,這麼寫就不對了 …
Date date = new Date(13, 8, 2);
DateFormat dateFormat = DateFormat.getDateInstance();
System.out.printf("Taiwan Java Developer Day is %s.%n",
dateFormat.format(date));
如果真要用 Date
建構式建立實例,那年份的部份,必須是西元年減去 1900,也就是說,如果你想表示的是 2013 年,那要 new Date(113, 8, 2)
才對,很奇怪對吧!實際上這個建構式早就被廢棄(Deprecated),編譯時你應該看一下警示訊息(不過應該很多人根本不看的吧!)…
應該會有人告訴你,如果你要設置日期,應該使用 java.util.Calendar
,像是這樣 …
Calendar calendar = Calendar.getInstance();
calendar.set(2013, 8, 2);
DateFormat dateFormat = DateFormat.getDateInstance();
System.out.printf("Taiwan Java Developer Day is %s.%n",
dateFormat.format(calendar.getTime()));
如果你想表達的還是 "2013-08-02"
,這麼寫就又不對了 … Calendar
的 set
方法第二個參數,數字起算是從 0 開始,所以要表示 8 月應該給 7 這個數字才對,不想搞混的話,就用個列舉常數吧!
calendar.set(2013, Calendar.AUGUST, 2);
Calendar
這個東西,是有一些計算日期時間的方法,不過還是得小心有些陷阱,下面這個程式是用來計算兩個日期之間的天數:
public static void main(String[] args) {
Calendar birth = Calendar.getInstance();
birth.set(1975, Calendar.MAY, 26);
Calendar now = Calendar.getInstance();
System.out.println(daysBetween(birth, now));
System.out.println(daysBetween(birth, now)); // 顯示 0?
}
public static long daysBetween(Calendar begin, Calendar end) {
long daysBetween = 0;
while(begin.before(end)) {
begin.add(Calendar.DAY_OF_MONTH, 1);
daysBetween++;
}
return daysBetween;
}
daysBetween
有點問題,如果連續計算兩個 Date
實例的話,第二次會取得 0,什麼情況下,會想要連續計算呢?總是會有這類的情況 … 無論如何,因為 Calendar
狀態是可變的,考慮會重複計算的場合,最好是複製出一個新的 Calendar
:
public static long daysBetween(Calendar begin, Calendar end) {
Calendar calendar = (Calendar) begin.clone(); // 複製
long daysBetween = 0;
while(calendar.before(end)) {
calendar.add(Calendar.DAY_OF_MONTH, 1);
daysBetween++;
}
return daysBetween;
}
陷阱還蠻多的 … 實際要探討下去,其實問題會更多,總之 …
雖然叫作 Date
,然而 Date
實例真正代表的並不是日期,如果你認真去解讀它,
Date
實例真正代表的概念,最接近的應該是特定的瞬時(a specific instant in time),時間精度是毫秒,例如 … 1375430498832 代表從 “the epoch”,也就是 UTC 時間 1970 年 1 月 1 日零時零分零毫秒至今經過 1375430498832 毫秒數的特定瞬時,嗯 … “the epoch” 與 UTC 這兩個玩意兒後面也都還要說明 …
總之,在 JDK 1.1 之前,Date 可用來將特定的瞬時剖析為年、月、日、時、分、秒等,不過值的表示有些奇怪,像是 …
- 年的數字表示西元年減去 1900
- 時、分、秒從 0 開始計數可以理解,那為什麼月也是從 0 開始計數?
後來,從 JDK 1.1 開始,將特定的瞬時剖析為年、月、日、時、分、秒等的方法都被廢棄了,看來 Date
單純可以表示瞬時了?事實上並非如此,Date
的 setTime
方法沒有被廢棄,也就是說,Date
的狀態仍是可變的,如果你在 API 之間傳遞它而不想改變它的狀態,就得祈禱 API 別去動它,另一方面,要手動利用 Date
計算日期時間,實在是太麻煩也容易出錯 …
Calendar
是提供了一些計算日期時間的方法,不過 … 使用 Calendar
實在是太麻煩、太痛苦了,你得用一堆列舉常數,像是 YEAR
、MONTH
、DAY_OF_MONTH
、HOUR
等,不然就得小心那些從 0 開始計算的日期時間,像是月份,另一方面,Calendar
狀態可變,有時也會造成前述的類似問題 …
至於 Calendar
相關的 API 為什麼這麼難用,這當中還有一些八掛歷史 … 查看一下 Calendar
的原始碼,你會看到這個註解:
/*
* (C) Copyright Taligent, Inc. 1996-1998 - All Rights Reserved
* (C) Copyright IBM Corp. 1996-1998 - All Rights Reserved
*
* The original version of this source code and documentation is copyrighted
* and owned by Taligent, Inc., a wholly-owned subsidiary of IBM. These
* materials are provided under terms of a License Agreement between Taligent
* and Sun. This technology is protected by multiple US and International
* patents. This notice and attribution to Taligent may not be removed.
* Taligent is a registered trademark of Taligent, Inc.
*
*/
有些人可能知道,Calendar
相關 API 是 IBM 捐出的,只是 Taligent 是什麼單位?過去有篇文章 The Seven Habits of Highly Dysfunctional Design,其中 Useless data types: Date, Date, Time and Timestamp 有談到一些 Calendar
的歷史,可惜的是文章已經消失在網路之中了…不過,有興趣還是可以搜尋一下「Java Calendar IBM Taligent」,還是可以找到些蛛絲馬跡…
實際上,無論你是使用 Date
、Calendar
,或者是使用後面要介紹的 Joda-Time、JSR310 等,都得瞭解更多日期時間的歷史與概念,才能清楚知道那些 API 代表了什麼,這會是下篇要討論的內容 …