理解 Java 時間, 日期 - Time and date in Java

關於時間,在JavaDoc中談論比較多文字的是UTC、UT、GMT、TimeZone等

下面是科學的對它們的簡單解釋。 
[b]UTC[/b]:科學紀年,時間採自原子時鐘。在每過一兩年會有一個跳秒,在某個跳點,一分鐘有61秒
[b]UT[/b]: GMT格林威治時間的科學學名,取自天文學觀測。GMT 是標準的“民間”名稱;UT 是相同標準的“科學”名稱

但java中, GMT的意思是有所不同的。 文中會介紹它們的意義。

[b][i]UTC[/i][/b]
如果我們要回答 "when" 而不是 “30秒之後”, 我們需要一種方法能夠表示任何一個時間點。 我們可以通過使用時間刻度 -- 就像尺子 -- 達到我們的目的。在一把尺子的範圍內, 它有零刻度, 正刻度(可能也有負刻度)。對於時間, 我們可以使用相同的方法。 我們任意定義一個時間爲 “0”。 通過這個零點, 我們可以表示將來的任意時間點, 當然也可以表示過去的時間點。 這就是 UTC (Coordinated Universal Time)所以做的功能。 通過它, 我們能給每一秒命名。例如 "2010-1-13 16:34:58.000 UTC". UTC總是增長的, 它從不會返回。 有時候,一分鐘並不總是60秒, 可能爲59秒或者61秒。 我們把這些秒叫做跳秒, 他們用於
糾正時間。
[b]
[i]通過操作系統表示時間[/i][/b]
在IT系統裏, 有時間, 我們需要準確的表示一個時間點。 比如
[list]
[*] 文件上次更改的時間
[*] 啓動一個設備
[*] 什麼時候發送的郵件
[/list]

這些時間點, 都有一個事件的特徵。 如果兩個時間是在同一時刻發生的。 不管一個是發生在上海, 另一個發生在倫敦。 它們都是發生在同一時刻的。與時區沒有關係。 我們經常使用操作系統時間去表示。操作系統的時間戳是一個從1970-1-1 UTC 零點到現在的整型數字, Java中的java.util.Date 類有效的封裝了這個時間戳。它跟時區是沒有關係的。
[b]注意:Date 的 toString 方法會用JVM所在系統的時區把時間打印出來。[/b]

[b][i]時區 -- Time Zones [/i][/b]

您肯定聽說過時區, 但您真正理解時區的意義嗎? 把他們理解爲一種測量時間的單位。就像測量長度的單位。 1米的長度是1000毫米。不管你說成1米或者1000毫米,長度是不會變化的。對於時間也是一樣。 一個確切的時間點不會因爲在不用的時區下而變化。只是表達不同而已。所有時區的母親是 Greenwich Mean Time (GMT). GMT是UTC使用的時區。 所有時區的偏移量, 都是通過與GMT的偏移來表示的。 往東的時區有正的偏移量, 那麼往西的話就是負的偏移量了。同樣我們還可以跟長度比較。 米是所有長度單位的母親。
1mm = 1/1000m, 1km=1000m.
轉爲時區的話, 也是同樣的道理:
18:00GM = 18:00+00:00 = 19:00+01:00 = 17:00-01:00

Java 中java.util.TimeZone能夠表示兩個內容。

[list]
[*] A time zone
[*] A time zone database of a location
[/list]

時區不是通過天文學家劃分的, 而是政治家。這些劃分會變化。 這就是爲什麼許多城市保留一個歷史時區數據庫。 許多國家在一年中會跟改時區兩次: 夏令時(daylight savings time) 只是一個time zone. 許多關於時間的應用中, 通過TimeZone.getTimeZone("Europe/Paris")使用a location database。 這樣可以解決
夏令時中的很多問題。 當然了, 應用也可以簡單的使用確切地時區去獲得TimeZone: TimeZone.getTimeZone("GMT+04:30").

在java.util.Calendar和java.text.DateFormat正確的使用 time zone是對應用中時區的安全問題至關重要的。應用的時間國際化, 不僅僅是支持不同的LocaleS. 同樣需要支持不同的TimeZoneS. 所有, 應用中Locale依賴的,  同樣也是TimeZone依賴的。需要注意的一個地方是TimeZone.getTimeZone()不會因爲不認識傳入的時區名字而拋出Exception, 而僅僅是返回GMT.如果一個時區的名字來自於不安全的地方,比如用戶輸入界面, 那麼我們需要驗證時區名字是否正確。

下面我們來研究Java API 中的時間類.
[b]
[i] java.util.Date, java.sql.Date 和java.sql.Timestamp 的關係[/i][/b]

通過前面的介紹,我們知道java.util.Date是對UTC時間的一個封裝。來看看它的構造器

public Date() {
this(System.currentTimeMillis());
}

其實, 就是一個long型數據的封裝。
而sql.Date和sql,Timestamp 是對util.Date的包裝 (繼承)。與sql中的date和timestamp一致, 可以直接插入數據庫。
這裏說下Timestamp. 此類型由 java.util.Date 和單獨的毫微秒值組成。只有整數秒纔會存儲在 java.util.Date 組件中。小數秒(毫微秒)是獨立存在的。傳遞不是 java.sql.Timestamp 實例的對象時,Timestamp.equals(Object) 方法永遠不會返回 true,因爲日期的毫微秒組件是未知的。因此,相對於 java.util.Date.equals(Object) 方法而言,Timestamp.equals(Object) 方法是不對稱的。此外,hashcode 方法使用底層 java.util.Date 實現並因此在其計算中不包括毫微秒。
鑑於 Timestamp 類和上述 java.util.Date 類之間的不同,建議代碼一般不要將 Timestamp 值視爲 java.util.Date 的實例。Timestamp 和 java.util.Date 之間的繼承關係實際上指的是實現繼承,而不是類型繼承。

由於Date其實是個long型數字, 但人們往往需要日曆型去表示時間。 當然,Calendar爲我們封裝了很多方便的方法。 其實Date也封裝了獲得日期的方法, 但已被廢棄不用了。 這時,我們可以通過DateFormat來format Date.
下面的例子, 用不同國家的默認格式顯示時間。

Date date = new Date();
Locale localeEN = Locale.US;
Locale localeCH = Locale.CHINA;

// Get a date time formatter for display in China.
DateFormat fullDateFormatCH = DateFormat.getDateTimeInstance(
DateFormat.FULL, DateFormat.FULL, localeCH);

// Get a date time formatter for display in the U.S.
DateFormat fullDateFormatEN = DateFormat.getDateTimeInstance(
DateFormat.FULL, DateFormat.FULL, localeEN);

System.out.println("Locale: " + localeCH.getDisplayName());
System.out.println(fullDateFormatCH.format(date));
System.out.println("Locale: " + localeEN.getDisplayName());
System.out.println(fullDateFormatEN.format(date));

輸出的結果是:
Locale: Chinese (China)
2011年1月13日 星期四 下午05時54分23秒 CST
Locale: English (United States)
Thursday, January 13, 2011 5:54:23 PM CST

由於使用默認TimeZone, 我們跟美國日期一樣。 下面,我們加上不同TimeZone, 來看看美國和GMT時區的這個時候日期是多少。

Date date = new Date();
Locale localeEN = Locale.US;
Locale localeCH = Locale.CHINA;
Locale locale = Locale.ENGLISH;

// Get a date time formatter for display in China.
DateFormat fullDateFormatCH = DateFormat.getDateTimeInstance(
DateFormat.FULL, DateFormat.FULL, localeCH);

// Get a date time formatter for display in the U.S.
DateFormat fullDateFormatEN = DateFormat.getDateTimeInstance(
DateFormat.FULL, DateFormat.FULL, localeEN);

DateFormat fullDateFormat = DateFormat.getDateTimeInstance(
DateFormat.FULL, DateFormat.FULL, locale);

TimeZone timeZoneChina = TimeZone.getDefault();
TimeZone timeZoneGMT = TimeZone.getTimeZone("GMT");
TimeZone timeZoneUS = TimeZone.getTimeZone("America/Whitehorse");

fullDateFormatCH.setTimeZone(timeZoneChina);
fullDateFormatEN.setTimeZone(timeZoneUS);
fullDateFormat.setTimeZone(timeZoneGMT);

System.out.println(" " + timeZoneChina.useDaylightTime() );
System.out.println(" China " + fullDateFormatCH.format(date));
System.out.println(" US " + fullDateFormatEN.format(date));
System.out.println(" GMT " + fullDateFormat.format(date));

輸出的結果是:
China 2011年1月13日 星期四 下午06時14分35秒 CST
US Thursday, January 13, 2011 2:14:35 AM PST
GMT Thursday, January 13, 2011 10:14:35 AM GMT

把String轉化爲日期, 這在GUI或者XML相關傳遞時間中經常見到。 我們可以使用DateFormat的一個默認實現SimpleDateFormat來完成我們的需求。 例如:

DateFormat indfm = new SimpleDateFormat("MM/dd/yyyy HH'h'mm");
indfm.setTimeZone(TimeZone.getTimeZone("America/Whitehorse"));
Date purchaseDate = indfm.parse("12/31/2007 20h15");

DateFormat outdfm = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
outdfm.setTimeZone(TimeZone.getTimeZone("GMT"));
System.out.println ( outdfm.format(purchaseDate) +" GMT");

輸出的結果爲:
2008-01-01 04:15:00 GMT

下圖是常用的日期和時間模式:
[img]http://dl.iteye.com/upload/attachment/390765/36615aea-a25c-38f8-8d88-68bf29baa5ca.jpg[/img]
模式字母的意義:
[img]http://dl.iteye.com/upload/attachment/390775/81ac6703-4dd5-39fa-b670-d4a045fa2269.jpg[/img]
這裏介紹個取得正確TimeZone的簡單技巧。 我們往往知道一個國家的時區, 但不知道這個時區的名字。 這時,我們可以簡單的遍歷下這個時區的所有名字來獲得合法的名字。


//+8 * 60 * 60 * 1000 或者 -7 * 60 * 60 * 1000
String[] ids = TimeZone.getAvailableIDs(0 * 60 * 60 * 1000);
if (ids.length == 0)
System.exit(0);

for (int i = 0; i < ids.length; i++) {
System.out.println("ids: " + ids[i]);
}


[b][i]java.util.Calendar[/i][/b]

這是語言環境敏感類,包含了TimeZone 信息. Calendar 提供了一個類方法 getInstance,以獲得此類型的一個通用的對象。Calendar 的 getInstance 方法返回一個 Calendar 對象,其日曆字段已由當前日期和時間初始化:

Calendar rightNow = Calendar.getInstance();
Calendar 對象能夠生成爲特定語言和日曆風格實現日期-時間格式化所需的所有日曆字段值,例如,日語-格里高裏歷,日語-傳統日曆。Calendar 定義了某些日曆字段返回值的範圍,以及這些值的含義。例如,對於所有日曆,日曆系統第一個月的值是 MONTH == JANUARY。其他值是由具體子類(例如 ERA)定義的。

[b]認識下日曆[/b]
1.

Calendar now = Calendar.getInstance();
TimeZone timeZoneUS = TimeZone.getTimeZone("America/Whitehorse");
Calendar us_now = new GregorianCalendar(timeZoneUS);
System.err.println(" China timestamp: " + now.getTimeInMillis());
System.err.println(" U timestamp: " + us_now.getTimeInMillis());

System.err.println(" China hour: " + now.get(Calendar.HOUR_OF_DAY));
System.err.println(" US Hour: " + us_now.get(Calendar.HOUR_OF_DAY));

輸出結果是:
China timestamp: 1294915267581
U timestamp: 1294915267581
China hour: 18
US Hour: 2

Calendar.getTimeInMillis() 方法,返回的是UTC時間戳, 所有, 不管處於哪個時區, 這個時間戳是相同的。
但是, Calendar.get(int field) 方法, 可返回當地時區的時間表示。 不用時區的話, 返回的值肯定是不同的。 我們此時是下午6點, 美國是凌晨2點。

那麼,日期中的這些年,月,日, 小時,分鐘,秒, 毫秒是怎樣計算出來的。 因爲TimeZone擁有偏移量 (包含夏令時的偏移量), 那麼, UTC跟不同用時區的偏移量進行計算, 就能得出不同時期的日曆表示。 在Calendar 類中, 通過方法 computeFields來完成。我們來簡單看下Calendar類的一個默認實現GregorianCalendar的 computeFields 方法長什麼樣:

/**
* Converts the time value (millisecond offset from the <a
* href="Calendar.html#Epoch">Epoch</a>) to calendar field values.
* The time is <em>not</em>
* recomputed first; to recompute the time, then the fields, call the
* <code>complete</code> method.
*
* @see Calendar#complete
*/
protected void computeFields() {
int mask = 0;
if (isPartiallyNormalized()) {
// Determine which calendar fields need to be computed.
mask = getSetStateFields();
int fieldMask = ~mask & ALL_FIELDS;
// We have to call computTime in case calsys == null in
// order to set calsys and cdate. (6263644)
if (fieldMask != 0 || calsys == null) {
mask |= computeFields(fieldMask,
mask & (ZONE_OFFSET_MASK|DST_OFFSET_MASK));
assert mask == ALL_FIELDS;
}
} else {
mask = ALL_FIELDS;
computeFields(mask, 0);
}
// After computing all the fields, set the field state to `COMPUTED'.
setFieldsComputed(mask);
}

具體的計算還是比較複雜, 有興趣的朋友可以進一步研究。 這裏我想強調的是, 什麼時候會調用這個方法。 請參閱下圖
[img]http://dl.iteye.com/upload/attachment/390463/8ccdc79b-02da-3de7-9894-6bfad413e9ae.jpg[/img]

[b] 同一個時間, 使用Calendar來顯示不同時區的時間[/b]

比如,通過日曆, 來看看美國現在的時間表示是什麼。

package util;

public class TimeTest {

public static Date getDateInTimeZone(Date currentDate, String timeZoneId) {
TimeZone tz = TimeZone.getTimeZone(timeZoneId);
Calendar mbCal = new GregorianCalendar(tz);

mbCal.setTimeInMillis(currentDate.getTime());

Calendar cal = Calendar.getInstance();
cal.set(Calendar.YEAR, mbCal.get(Calendar.YEAR));
cal.set(Calendar.MONTH, mbCal.get(Calendar.MONTH));
cal.set(Calendar.DAY_OF_MONTH, mbCal.get(Calendar.DAY_OF_MONTH));
cal.set(Calendar.HOUR_OF_DAY, mbCal.get(Calendar.HOUR_OF_DAY));
cal.set(Calendar.MINUTE, mbCal.get(Calendar.MINUTE));
cal.set(Calendar.SECOND, mbCal.get(Calendar.SECOND));
cal.set(Calendar.MILLISECOND, mbCal.get(Calendar.MILLISECOND));

return cal.getTime();
}

public static void main(String[] args) {

// Canada/Central
String timeZoneId = "Canada/Central";
Date now = new Date();
// System.out.println("Getting Time in the timezone="+timeZoneId);
System.out.println("Current Time there="+getDateInTimeZone(now,timeZoneId));
}

}


輸出結果是:
Current Time here =Thu Jan 13 19:10:18 CST 2011
Current Time there=Thu Jan 13 05:10:18 CST 2011

請注意方法Calendar.getTime(). 看看它的源碼:

public final Date getTime() {
return new Date(getTimeInMillis());
}

它其實就是UTC時間戳。 前面有提到, Date的toString方法用當地時區格式打印出來。 因爲同一時間點, UTC是相同的。 我們要用我們的TimeZone打印出美國的現在時間, 必須改變UTC值。 mbCal的時期(年,月,日,小時,分鐘,秒 。。。 )其實已經發生了變化。setTimeInMillis 方法會計算偏移量。 這裏我表達的可能不清楚, 如果大家有問題的話, 可以給我留言。

[b][i]日曆的字段操作[/i][/b]
日曆常用的一個地方, 就是字段的操作了。 可以使用三種方法更改日曆字段:set()、add() 和 roll()。下面會具體介紹這三個方法的區別。

[b]set(f, value) [/b]將日曆字段 f 更改爲 value。此外,它設置了一個內部成員變量,以指示日曆字段 f 已經被更改。儘管日曆字段 f 是立即更改的,但是直到下次調用 get()、getTime()、getTimeInMillis()、add() 或 roll() 時纔會重新計算日曆的時間值(以毫秒爲單位)。因此,多次調用 set() 不會觸發多次不必要的計算。使用 set() 更改日曆字段的結果是,[b]其他日曆字段也可能發生更改[/b],這取決於日曆字段、日曆字段值和日曆系統。此外,在重新計算日曆字段之後,get(f) 沒必要通過調用 set 方法返回 value 集合。具體細節是通過具體的日曆類確定的。

示例:假定 GregorianCalendar 最初被設置爲 1999 年 8 月 31 日。調用 set(Calendar.MONTH, Calendar.SEPTEMBER) 將該日期設置爲 1999 年 9 月 31 日。如果隨後調用 getTime(),那麼這是解析 1999 年 10 月 1 日的一個暫時內部表示。但是,在調用 getTime() 之前調用 set(Calendar.DAY_OF_MONTH, 30) 會將該日期設置爲 1999 年 9 月 30 日,因爲在調用 set() 之後沒有發生重新計算。


Calendar cal = Calendar.getInstance();

System.out.println("Current Timezone="+cal.getTimeZone().getDisplayName());

cal.set(Calendar.MONTH, Calendar.DECEMBER);
cal.set(Calendar.DAY_OF_MONTH, 31);

System.out.println("Calendar raw = " + cal.getTime());

cal.set(Calendar.MONTH, Calendar.FEBRUARY);
System.out.println("Calendar after change = 2 " + cal.getTime());


輸出結果是
Current Timezone=China Standard Time
Calendar raw = Sat Dec 31 19:26:51 CST 2011
Calendar after change = 2 Thu Mar 03 19:26:51 CST 2011

[b]請注意[/b], 結果不是2月31日。因爲沒有2月31日。 再繼續看下面的代碼:

Calendar cal = Calendar.getInstance();

System.out.println("Current Timezone="+cal.getTimeZone().getDisplayName());

cal.set(Calendar.MONTH, Calendar.DECEMBER);
cal.set(Calendar.DAY_OF_MONTH, 31);

System.out.println("Calendar raw = " + cal.getTime());

cal.set(Calendar.MONTH, Calendar.FEBRUARY);
cal.set(Calendar.DAY_OF_MONTH, 28);
System.out.println("Calendar after change = " + cal.getTime());

輸出的結果是:
Current Timezone=China Standard Time
Calendar raw = Sat Dec 31 19:31:04 CST 2011
Calendar after change = 2 Mon Feb 28 19:31:04 CST 2011

[b]注意:[/b] 結果不是3月28號。

[b]add(f, delta)[/b] 將 delta 添加到 f 字段中。這等同於調用 set(f, get(f) + delta),但要帶以下兩個調整:

Add 規則 1。調用後 f 字段的值減去調用前 f 字段的值等於 delta,以字段 f 中發生的任何溢出爲模。溢出發生在字段值超出其範圍時,結果,下一個更大的字段會遞增或遞減,並將字段值調整回其範圍內。

Add 規則 2。如果期望某一個更小的字段是不變的,但讓它等於以前的值是不可能的,因爲在字段 f 發生更改之後,或者在出現其他約束之後,比如時區偏移量發生更改,它的最大值和最小值也在發生更改,然後它的值被調整爲儘量接近於所期望的值。更小的字段表示一個更小的時間單元。HOUR 是一個比 DAY_OF_MONTH 小的字段。對於不期望是不變字段的更小字段,無需進行任何調整。日曆系統會確定期望不變的那些字段。

此外,與 set() 不同,add() 強迫日曆系統立即重新計算日曆的毫秒數和所有字段。
示例:假定 GregorianCalendar 最初被設置爲 1999 年 8 月 31 日。調用 add(Calendar.MONTH, 13) 將日曆設置爲 2000 年 9 月 30 日。Add 規則 1 將 MONTH 字段設置爲 September,因爲向 August 添加 13 個月得出的就是下一年的 September。因爲在 GregorianCalendar 中,DAY_OF_MONTH 不可能是 9 月 31 日,所以 add 規則 2 將 DAY_OF_MONTH 設置爲 30,即最可能的值。儘管它是一個更小的字段,但不能根據規則 2 調整 DAY_OF_WEEK,因爲在 GregorianCalendar 中的月份發生變化時,該值也需要發生變化。

Calendar cal = Calendar.getInstance();
System.out.println("Current Timezone="+cal.getTimeZone().getDisplayName());
cal.set(Calendar.MONTH, Calendar.JANUARY);
cal.set(Calendar.DAY_OF_MONTH, 31);
System.out.println("Calendar raw = " + cal.getTime());
cal.add(Calendar.MONTH, 1);
System.out.println("Calendar after change = " + cal.getTime());
cal.add(Calendar.MONTH, 1);
System.out.println("Calendar after change = " + cal.getTime());

結果是:
Current Timezone=China Standard Time
Calendar raw = Mon Jan 31 19:50:23 CST 2011
Calendar after change = Mon Feb 28 19:50:23 CST 2011
Calendar after change = Mon Mar 28 19:50:23 CST 2011

[b]roll(f, delta)[/b] 將 delta 添加到 f 字段中,但不更改更大的字段。這等同於調用 add(f, delta),但要帶以下調整:

Roll 規則。在完成調用後,更大的字段無變化。更大的字段表示一個更大的時間單元。DAY_OF_MONTH 是一個比 HOUR 大的字段。


Calendar cal = Calendar.getInstance();

System.out.println("Current Timezone="+cal.getTimeZone().getDisplayName());
cal.set(Calendar.MONTH, Calendar.DECEMBER);
cal.set(Calendar.DAY_OF_MONTH, 31);
System.out.println("Calendar raw = " + cal.getTime());
cal.roll(Calendar.MONTH, 1);
System.out.println("Calendar after change = " + cal.getTime());

結果是:
Current Timezone=China Standard Time
Calendar raw = Sat Dec 31 20:05:25 CST 2011
Calendar after change = Mon Jan 31 20:05:25 CST 2011
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章