2014年3月31日 星期一

Java 複習筆記 : 日期與時間

今天要繼續寫常用類別庫的日期部分時, 原本是想照搬 Javascript 版本的 get_date_time(), 改寫為 getDateTime() 就可以了, 因為 Java 裡面的 java.util.Date 跟 Javascript 裡處理日期時間的 Date 物件用法幾乎一模一樣, 改寫起來應該很輕鬆啦, 但編譯時卻發現如下錯誤 :

Note: JT.java uses or overrides a deprecated API.
Note: Recompile with -Xlint:deprecation for details.

檢查 API 才發現, 原來 java.util.Date 的方法大部分早已經被廢棄 (deprecated), 參見 :

http://docs.oracle.com/javase/7/docs/api/java/util/Date.html

一. java.util.Date類別 :

Date 物件僅剩下列四個是常用的方法 :
  1. after(Date when) : 檢查日期物件時間是否在本物件後 (true/false)   
  2. before(Date when) : 檢查日期物件時間是否在本物件前 (true/false)   
  3. getTime() : 傳回自 1970/1/1 以來之毫秒數
  4. setTime(int ms) : 將時間設定為 1970/1/1 後之毫秒數
  5. toString() : 把 Date 物件轉成字串
例如 :

import java.util.*;
public class mytest {
  public static void main(String[] args) {
    Date d=new Date();                    //建立目前時間之 Date 物件
    System.out.println(d.getTime());   //輸出 1396313616224
    System.out.println(System.currentTimeMillis());  //輸出 1396313616224
    System.out.println(d.toString());   //輸出 Tue Apr 01 08:53:36 CST 2014
    Date d1=new Date(1);     //建立時間戳記為 1ms 之 Date 物件 (自 1970/1/1)
    System.out.println(d1.before(d));  //輸出 true
    System.out.println(d.before(d1));  //輸出 false
    System.out.println(d1.after(d));     //輸出 false
    System.out.println(d.after(d1));     //輸出 true
    d1.setTime(4396267507921L);   //須為長整數
    System.out.println(d.after(d1));     //輸出 false
    }
  }

要取得自 1970/1/1 以來的毫秒數也可以用 System.currentTimeMillis() 方法, 事實上, 在 new Date() 時, 也是使用此方法取得系統時間的.

Date 類別其他以前常用的 getYear(), getDate() 等都被廢棄了, 我離開 Java 太久了, 竟然一無所知. 現在要改用 java.util.Calendar 類別, 其 API 如下 :

http://docs.oracle.com/javase/7/docs/api/java/util/Calendar.html

二. java.util.Calendar 類別 : 

使用 Calender 時只要呼叫其靜態方法 getInstance() 就會傳回一個 Calendar 物件 :

Calendar c=Calendar.getInstance();

要取得年月日時分秒資訊可直接擷取其欄位, 傳回值均為整數, 我們可將其放入陣列 :

    int[] a={c.get(Calendar.YEAR),
                 c.get(Calendar.MONTH),
                 c.get(Calendar.DAY_OF_MONTH),
                 c.get(Calendar.HOUR_OF_DAY),
                 c.get(Calendar.MINUTE),
                 c.get(Calendar.SECOND)
                 };

注意這裡 DAY_OF_MONTH 之值為 0~11, 與 Javascript 的 Date 物件之 getMonth() 一樣, 所以必須加一轉成我們習慣的 1~12 月. 現在是 3 月 31 日, 下面的範例卻顯示 2 月 31 日 :

import java.util.*;
public class mytest {
  public static void main(String[] args) {
    Calendar c=Calendar.getInstance();
    int[] a={c.get(Calendar.YEAR),
             c.get(Calendar.MONTH),
             c.get(Calendar.DAY_OF_MONTH),
             c.get(Calendar.HOUR_OF_DAY),
             c.get(Calendar.MINUTE),
             c.get(Calendar.SECOND)
             };
    for (int i:a) {System.out.print(i + " ");}  //輸出 2014 2 31 16 6 33
    }
  }

如果要指定日期時間, 則要呼叫 set() 方法,

set(int year, int month, int date, int hourOfDay, int minute, int second)

注意, 其中的月份也是要傳入實際月份減一才對.

我的常用類別庫對日期時間的要求其時很簡單, 最常用的是傳回目前的日期時間字串, 以便寫入資料庫或 log 檔中, 慣用格式為 "2014-03-31 13:24:41".  這可以用下列靜態方法達成 :

import java.util.Calendar;
public class mytest {
  public static void main(String[] args) {
    System.out.println(getDateTime());  //輸出 2014-03-31 16:15:45
    System.out.println(getDateTime(2014,3,31,0,59,59)); //輸出 2014-03-31 00:59:59
    }
  public static String getDateTime() {  //無參數=傳回現在時間
    Calendar c=Calendar.getInstance();
    return getYMDHMS(c);
    }
  public static String getDateTime(int Y, int M, int D, int H, int m, int S) { //指定時間
    Calendar c=Calendar.getInstance();
    c.set(Y, --M, D, H, m, S);  //傳進來的實際月份要減 1
    return getYMDHMS(c);
    }
  public static String getYMDHMS(Calendar c) { //輸出格式製作
    int[] a={c.get(Calendar.YEAR),
             c.get(Calendar.MONTH),
             c.get(Calendar.DAY_OF_MONTH),
             c.get(Calendar.HOUR_OF_DAY),
             c.get(Calendar.MINUTE),
             c.get(Calendar.SECOND)
             };
    StringBuffer sb=new StringBuffer();
    sb.append(a[0]);
    if (a[1]<9) {sb.append("-0" + (a[1] + 1));}   //加 1 才會得到實際月份
    else {sb.append("-" + (a[1] + 1));}
    if (a[2]<10) {sb.append("-0" + (a[2]));}
    else {sb.append("-" + (a[2]));}
    if (a[3]<10) {sb.append(" 0" + (a[3]));}
    else {sb.append(" " + (a[3]));}
    if (a[4]<10) {sb.append(":0" + a[4]);}
    else {sb.append(":" + a[4]);}
    if (a[5]<10) {sb.append(":0" + a[5]);}
    else {sb.append(":" + a[5]);}
    return sb.toString();
    }
}

可見對於小於 10 之值前端補 0, 就會傳回我要的標準格式. 注意, 上面的 getDateTime() 方法已經針對月份做加 1 處理過了, 因此呼叫時要傳入實際的月份, 不須再加 1 了.

Calendar 也跟 Date 一樣有一個 getTime() 方法, 但它會傳回一個 Date 物件, 而非自 1970/1/1 的毫秒數. Calendar 類別中相同功能的方法為 getTimeInMillis(). 同樣的, Calendar 也有一個 setTime() 方法, 但其傳入參數不是一個長整數, 而是一個 Date 物件 :

import java.util.*;
public class mytest {
  public static void main(String[] args) {
    Calendar c=Calendar.getInstance();
    Date d=c.getTime();  //傳回 Date 物件
    System.out.println(d.getTime());             //輸出 1396267507921
    System.out.println(c.getTimeInMillis());    //輸出 1396267507921
    d.setTime(1396277507921L);  //傳入長整數
    c.setTime(d);  //傳入 Date 物件
    int[] a={c.get(Calendar.YEAR),
             c.get(Calendar.MONTH),
             c.get(Calendar.DAY_OF_MONTH),
             c.get(Calendar.HOUR_OF_DAY),
             c.get(Calendar.MINUTE),
             c.get(Calendar.SECOND)
             };      
    for (int i:a) {System.out.print(i + " ");}  //輸出 2014 2 31 22 51 47
    }
  }       

Calendar 類別也有 before() 與 after() 用來比較兩個 Calendar 物件的時間先後, 例如

import java.util.*;
public class mytest {
  public static void main(String[] args) {
    Date d=new Date();
    Calendar c1=Calendar.getInstance();
    Calendar c2=Calendar.getInstance();
    d.setTime(1);     //設定時戳為 1ms
    c1.setTime(d);
    d.setTime(2);     //設定時戳為 2ms
    c2.setTime(d);
    System.out.println(c1.before(c2));  //輸出 true
    System.out.println(c1.after(c2));     //輸出 false
    }
  }       

Calendar 類別還有一個常用的應用是求指定日期是星期幾. 這個必須用到 Calendar.DAY_OF_WEEK 這個欄位, 其值為 0 (Sunday) ~ 6 (Saturday), 相對應 Calendar.SUNDAY~Calendar.SATURDAY. 透過判別 Calendar.DAY_OF_WEEK 之值, 即傳回 "星期日" ~ "星期六" 字串, 如下列範例所示 :

import java.util.*;
import java.text.*;
public class mytest {
  public static void main(String[] args) {
    System.out.println(getDayOfWeek("2014-04-01"));  //輸出 星期二
    }
  public static int[] parseDate(String date) {
    String[] d=date.split("-");
    int[] dt=new int[3];
    dt[0]=Integer.parseInt(d[0]);
    dt[1]=Integer.parseInt(d[1]);
    dt[2]=Integer.parseInt(d[2]);
    return dt;
    }
  public static String getDayOfWeek(String date) {
    int[] d=parseDate(date);
    Calendar c=Calendar.getInstance();
    c.set(d[0],d[1]-1,d[2]);  //這裡務必減 1
    String w="";
    switch(c.get(Calendar.DAY_OF_WEEK)) {
      case Calendar.SUNDAY :    {w="星期日";break;}
      case Calendar.MONDAY :    {w="星期一";break;}
      case Calendar.TUESDAY :   {w="星期二";break;}
      case Calendar.WEDNESDAY : {w="星期三";break;}
      case Calendar.THURSDAY :  {w="星期四";break;}
      case Calendar.FRIDAY :    {w="星期五";break;}
      case Calendar.SATURDAY :  {w="星期六";break;}
      }
    return w;
    }
  }


三. DateFormat 與 SimpleDateFormat 類別 :

這兩個類別與上面的 Date 跟 Calendar 源頭不同, 它們並非來自 java.util 類別庫, 而是源自 java.text.Format 類別, SimpleDateFormat 則是 DateFormat 的子類別.

java.lang.Object
    |__ java.text.Format
                |__ java.text.DateFormat
                           |__ java.text.SimpleDateFormat

這兩個類別主要是用來轉換日期字串與 Date 物件, 剖析日期字串可能因為格式錯誤而拋出例外, 所以使用 DateFormat 與 SimpleDateFormat 這兩個類別時, 必須匯入 ParseException 類別 :

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.text.ParseException;

或者乾脆 :

import java.text.*;

# http://docs.oracle.com/javase/7/docs/api/java/text/DateFormat.html
# http://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html

DateFormat 類別有四個欄位用來設定日期字串的顯示格式 (style) :

 欄位 說明
 DateFormat.SHORT (3) "3/31/14" 2014 年 3 月 31 日 (美式)
 DateFormat.MEDIUM (2) "Mar 31, 2014" 
 DateFormat.LONG (1) "March 31, 2014"
 DateFormat.SHORT (0) "Monday, March 31, 2014"

此四種格式是用來設定日期物件的顯示格式. 跟 Calendar 類別一樣, 要取得 DateFormat 物件可以直接呼叫類別方法 getDateTimeInstance() :

DateFormat df=DateFormat.getDateTimeInstance();

沒有傳入參數的話, 傳回的 DateFormat 物件會以預設的日期與時間格式與區域 (Locale) 來進行格式化. 呼叫 format() 方法即傳回值其格式化字串, 如下所示 :

import java.util.*;
import java.text.*;
public class mytest {
  public static void main(String[] args) {
    Date d=new Date();
    DateFormat df=DateFormat.getDateTimeInstance();  //取得預設格式化物件
    System.out.println(df.format(d));      //輸出 2014/4/1
    }
  }

也可以傳入日期與時間格式參數 :

getDateTimeInstance(int dateStyle, int timeStyle) 

這樣就會依照指定格式顯示, 這裡為了簡單起見, 傳入整數代碼 :

import java.util.*;
import java.text.*;
public class mytest {
  public static void main(String[] args) {
    Date d=new Date();
    DateFormat df_short=DateFormat.getDateTimeInstance(3,3);      //SHORT 格式
    DateFormat df_medium=DateFormat.getDateTimeInstance(2,2);  //MEDIUM 格式
    DateFormat df_long=DateFormat.getDateTimeInstance(1,1);       //LONG 格式
    DateFormat df_full=DateFormat.getDateTimeInstance(0,0);         //FULL 格式
    System.out.println(df_short.format(d));  //輸出 2014/4/1 上午 10:18
    System.out.println(df_medium.format(d));  //輸出 2014/4/1 上午 10:18:06
    System.out.println(df_long.format(d));  //輸出  2014年4月1日 上午10時18分06秒
    System.out.println(df_full.format(d));   //輸出 2014年4月1日 星期二 上午10時18分06秒 TST
    }
  }

可見預設是顯示本地的區域語言 (Locale), 如果要顯示英文, 則要傳入第三參數 locale :

getDateTimeInstance(int dateStyle, int timeStyle, Local locale) 

這就需要用到 java.util.Local 類別來建立一個 Locale 物件 :

Locale locale=new Locale("en", "US");

第一個參數為語言, 第二個參數是國家 :

Locale(String language, String country)

台灣的話, language 傳入 "zh", country 傳入 "zh_TW". 如下表所示  :

語言 Language Country
 美式英文 en US
 英式英文 en en_GB
 加式英文 en en_CA
 繁體中文 zh zh_TW
 日文 ja ja_JP
 韓文 ko ko_KR
 簡體中文 zh zh_CN
 德文 de de_DE
 義大利文 it it_IT
 法文 fr fr_FR
 加式法文 fr fr_CA

http://docs.oracle.com/javase/7/docs/api/java/util/Locale.html 

下列範例設定為英文格式輸出 :

import java.util.*;
import java.text.*;
public class mytest {
  public static void main(String[] args) {
    Date d=new Date();
    Locale locale=new Locale("en", "US");
    DateFormat df_short=DateFormat.getDateTimeInstance(3,3,locale);
    DateFormat df_medium=DateFormat.getDateTimeInstance(2,2,locale);
    DateFormat df_long=DateFormat.getDateTimeInstance(1,1,locale);
    DateFormat df_full=DateFormat.getDateTimeInstance(0,0,locale);
    System.out.println(df_short.format(d));   //輸出 4/1/14 11:05 AM
    System.out.println(df_medium.format(d));  //輸出 Apr 1, 2014 11:05:48 AM
    System.out.println(df_long.format(d));   //輸出 April 1, 2014 11:05:48 AM CST
    System.out.println(df_full.format(d)); //輸出 Tuesday, April 1, 2014 11:05:48 AM CST
    }
  }

可見傳入美國的 Locale 後就全部洋化了. 以上是把 Date 物件格式化為字串輸出, 反過來要把日期時間字串轉成 Date 物件該怎麼做? 這可以呼叫 parse() 方法來剖析字串, 此方法會傳回一個 Date 物件 :

Date parse(String source)

但日期時間字串格式不見得正確, 可能會拋出例外, 因此 parse() 必須放在 try catch 區塊裡面, 如下例所示 :

import java.util.*;
import java.text.*;
public class mytest {
  public static void main(String[] args) {
    Date d=new Date();
    Locale locale=new Locale("en", "US");
    DateFormat df_short=DateFormat.getDateTimeInstance(3,3,locale);
    DateFormat df_medium=DateFormat.getDateTimeInstance(2,2,locale);
    DateFormat df_long=DateFormat.getDateTimeInstance(1,1,locale);
    DateFormat df_full=DateFormat.getDateTimeInstance(0,0,locale);
    try {d=df_medium.parse("Apr 1, 2014 11:05:48 AM");}
    catch (ParseException e) {e.printStackTrace();}
    System.out.println(df_short.format(d));  //輸出 4/1/14 11:05 AM
    System.out.println(df_medium.format(d)); //輸出 Apr 1, 2014 11:05:48 AM
    System.out.println(df_long.format(d));    //輸出 April 1, 2014 11:05:48 AM CST
    System.out.println(df_full.format(d));  //輸出 Tuesday, April 1, 2014 11:05:48 AM CST
    }
  }

注意, 傳入的日期時間字串, 格式必須符合 DateFormat 之設定, 否則執行時期會拋出例外. 由於 DateFormat 只提供了四種格式, 因此無法產生我需要的 "YYYY-MM-DD HH:mm:SS" 格式, 這可以用 DateFormat 的子類別 SimpleDateFormat 來解決.

SimpleDateFormat 跟 DateFormat 一樣用在 Date 物件與格式字串的互轉, 但 SimpleDateFormat 定義了一些特定的格式字元, 可藉著格式字元來產生所需要的輸出. 參考 :

http://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html

與 DateFormat 不同的是, 建立一個 SimpleDateFormat 物件是必須呼叫其建構子 :

SimpleDateFormat sdf=new SimpleDateFormat();

此類別的 toPattern() 方法會傳回目前的格式字串, 而 applyPattern() 方法則可以傳入格式字串以套用新的格式, 如下例所示 :

import java.util.*;
import java.text.*;
public class mytest {
  public static void main(String[] args) {
    Date d=new Date();
    SimpleDateFormat sdf=new SimpleDateFormat();
    System.out.println(sdf.toPattern());  //輸出 yyyy/M/d a h:mm
    System.out.println(sdf.format(d));    //輸出 2014/4/1 下午 1:44
    sdf.applyPattern("yyyy-MM-dd HH:mm:ss");  //套用新格式
    System.out.println(sdf.format(d));  //輸出 2014-04-01 13:44:36
    }
  }

可見, 如果建立 SimpleDateFormat 物件時沒有指定格式, 則預設的格式字串為 "yyyy/M/d a h:mm". 也可以在建立物件時就指定格式字串與 Locale 物件 :

SimpleDateFormat(String pattern, Locale locale)

如下範例所示 :

import java.util.*;
import java.text.*;
public class mytest {
  public static void main(String[] args) {
    Date d=new Date();
    Locale locale=new Locale("it", "it_IT");  //義大利語
    String pattern="yyyy.MM.dd G 'at' HH:mm:ss z";
    SimpleDateFormat sdf=new SimpleDateFormat(pattern,locale);  //指定格式與區域語言
    System.out.println(sdf.toPattern()); //輸出 yyyy.MM.dd G 'at' HH:mm:ss z
    System.out.println(sdf.format(d)); //輸出 2014.04.01 dopo Cristo at 14:51:27 CST
    }
  }

輸出格式中的 G (Era designator 年代) 就顯示義大利文的 "dopo Cristo" (西元 : 主後).

既然 SimpleDateFormat 是 DateFormat 的子類別, 當然可以使用 format() 與 parse() 方法來互轉 Date 物件與日期時間字串, 如下列範例所示 :

import java.util.*;
import java.text.*;
public class mytest {
  public static void main(String[] args) {
    Date d=new Date();
    System.out.println(d.toString());  //輸出 Tue Apr 01 15:04:12 CST 2014
    String pattern="yyyy-MM-dd HH:mm:ss";
    SimpleDateFormat sdf=new SimpleDateFormat(pattern);
    System.out.println(sdf.format(d)); //輸出 2014-04-01 15:04:12
    try {d=sdf.parse("2013-12-25 23:59:59");}
    catch (ParseException e) {e.printStackTrace();}
    System.out.println(d.toString()); //輸出 Wed Dec 25 23:59:59 CST 2013
    }
  }

最後, 我們可以利用 SimpleDateFormate 來改良上面用 Calendar 類別寫的 getDateTime() 方法 :

import java.util.*;
import java.text.*;
public class mytest {
  public static void main(String[] args) {
    System.out.println(getDateTime());
    }
  public static String getDateTime() {
    SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    return sdf.format(new Date());
    }
  }

實在是太簡單了, 不須再去處理年月日時分秒了, 難怪取名為 "Simple" !

參考 :

# Java Calendar取得年(year)、月(month)、日(day)
# Java Gossip: 使用 Calendar
# 深入理解Java:SimpleDateFormat安全的时间格式化
# JAVA的日期時間取得 
# Java Gossip: 使用 Date、DateFormat
# Java 获得指定日期是一年中的第几天

1 則留言 :

Unknown 提到...

感謝~~~還好要寫前有看到您這篇
我差點像以前一樣直接用Date XD