【Joda-Time 與 JSR310 】(1)Date 與 Calendar 怎麼了?


【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",這麼寫就又不對了 … Calendarset 方法第二個參數,數字起算是從 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 單純可以表示瞬時了?事實上並非如此,DatesetTime 方法沒有被廢棄,也就是說,Date 的狀態仍是可變的,如果你在 API 之間傳遞它而不想改變它的狀態,就得祈禱 API 別去動它,另一方面,要手動利用 Date 計算日期時間,實在是太麻煩也容易出錯 …

Calendar 是提供了一些計算日期時間的方法,不過 … 使用 Calendar 實在是太麻煩、太痛苦了,你得用一堆列舉常數,像是 YEARMONTHDAY_OF_MONTHHOUR 等,不然就得小心那些從 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」,還是可以找到些蛛絲馬跡…

實際上,無論你是使用 DateCalendar,或者是使用後面要介紹的 Joda-Time、JSR310 等,都得瞭解更多日期時間的歷史與概念,才能清楚知道那些 API 代表了什麼,這會是下篇要討論的內容 …