Java中的時間和日期(上)

 

自從JDK 1.0開始,Java就提供了Date來處理時間和日期,作爲老古董自然有很多東西是過時的。然後出現了Calendar來解決了很多問題,但是Calendar使用比較複雜,並且有些反人類的地方。直到Java 8的出現,它吸收了Joda-Time庫的經驗,使得Java處理時間和日期變得比較”人性化”了。

本篇就來談談Java中的Date、Calendar,以及SimpleDateFormat的使用。在Java中的時間和日期(下)裏面再對比一下Java 8中的日期處理。

1. 古老的 java.util.Date

查看一下Date類的源碼,可以看到6個構造方法中,有4個已經添加了@Deprecated的標籤了,剩下沒過時的只有兩個:

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

public Date(long var1) {
    this.fastTime = var1;
}

其中,第一個構造方法調用的是System.currentTimeMillis()方法,這個方法返回的是一個long整數,表示GMT 1970年1月1日 00:00:00到現在所經歷的毫秒數。這個毫秒數我們暫且稱之爲milliseconds值,這個值很重要,無論是在Date類中還是Calendar類中,milliseconds值都是計算時間日期的基準。

怎麼獲得這個長整型的數呢?需要調用getTime()方法,注意並不是調用toString()方法,它們兩者的差異可以重下面的代碼中體現出來:

public static void main(String[] args) {
    Date date = new Date();
    System.out.println(date.getTime());     // 1467628171312
    System.out.println(date.toString());    // Mon Jul 04 18:29:31 CST 2016
}

而第二個構造方法的參數,表示創建的Date對象與GMT 1970年1月1日 00:00:00的時間差。比如,3600 * 1000毫秒是一小時,那麼我們用getTime()來驗證一下:

public static void main(String[] args) {
    Date date = new Date(3600 * 1000);
    System.out.println(date.getTime());     // 3600000
}

沒問題,然後我們調用一下toString()方法,看看返回的是什麼:

public static void main(String[] args) {
    Date date = new Date(3600 * 1000);
    System.out.println(date.toString());     // Thu Jan 01 09:00:00 CST 1970
}

嗯?竟然是9點,爲什麼不是1點呢?怎麼差了8個小時?有此疑問的童鞋都是看書不仔細的。而看書仔細的童鞋則會問“CST和GMT是什麼關係”之類的。因爲CST是指的北京時間(China Standard Time),而GMT是指的是格林尼治標準時間(Greenwich Mean Time)。由於北京處於東八區,比GMT早8小時,所以打印的時間指的是北京時間1970年1月1日上午9點。

談完了構造方法,下面說說Date這個古老的類中可以使用的方法。

    • boolean after(Date when)

      測試此日期是否在指定日期之後。

      boolean before(Date when)

      測試此日期是否在指定日期之前。

      Object clone()

      返回此對象的副本。

      int compareTo(Date anotherDate)

      比較兩個日期進行訂購。

      boolean equals(Object obj)

      比較兩個日期來平等。

      static Date from(Instant instant)

      Instant對象獲取一個 Date的實例。

    • long getTime()

      返回自1970年1月1日以來,由此 Date對象表示的00:00:00 GMT的毫秒 數 。

一段代碼解釋上面的方法:

public static void main(String[] args) {
    Date nowaday = new Date();
    System.out.println(nowaday.getTime());      // 1467633231471
    System.out.println(nowaday.toString());     // Mon Jul 04 19:53:51 CST 2016

    Date date = new Date(3600 * 1000);
    System.out.println(nowaday.before(date));   // false
    System.out.println(nowaday.after(date));    // true

    nowaday.setTime(3600 * 1000);
    System.out.println(nowaday);                // Thu Jan 01 09:00:00 CST 1970
}

Date類中的構造方法和常用的未過時的方法基本就是這些了。看了之後是不是覺得其實Date類能用的東西很少?很多日期的操作,比如獲取年月日啊,取這週週一的日期啊什麼的都很難實現,這時候就要使用到Calendar類,或者使用更牛逼的Java 8中的時間日期包。

 

2. 龐大的 java.util.Calendar

Calendar類的構造方法

與Date類不同,Calendar類是一個抽象類。其直接子類是GregorianCalendar。既然Calendar類是抽象類,那麼如獲取Calendar對象呢?可以使用下面的代碼來創建一個Calendar對象:

Calendar rightNow = Calendar.getInstance();

和Date一樣,創建好對象之後,便包含了當前日期和時間的信息。

其實,getInstance()這個靜態方法還有三個重載方法,在源碼中我們可以看到它們4個歡聚一堂的情景:

public static synchronized Calendar getInstance() {
    return new GregorianCalendar();
}
public static synchronized Calendar getInstance(Locale locale) {
    return new GregorianCalendar(locale);
}
public static synchronized Calendar getInstance(TimeZone timezone) {
    return new GregorianCalendar(timezone);
}
public static synchronized Calendar getInstance(TimeZone timezone, Locale locale) {
    return new GregorianCalendar(timezone, locale);
}

估計大家都能看出來,這4個重載方法的區別在於時區和地區的不同。而它們都是通過創建子類(GregorianCalendar)的對象來返回一個Calendar對象,這其實是多態的一種應用,也是設計模式中工廠模式的應用。也就是說,我們也可以像下面那樣來創建一個Calendar對象:

Calendar myCalendar = new GregorianCalendar();

關於時區和地區這裏不做詳細的介紹,只展示一下其用法:

public static void main(String[] args) {
    TimeZone timeZone = TimeZone.getTimeZone("GMT");
    Locale locale = Locale.CHINA;
    Calendar calendar = Calendar.getInstance(timeZone, locale);
}

Calendar的常用方法

get()和set()方法

Date對象可以通過getTime()方法返回 Long 類型 和toString()方法,能很容易的取到對應的時間信息。

Calendar對象中,getTime()方法返回的是一個Date對象,getTimeInMillis()方法返回 Long 類型,至於toString()方法返回的東西嘛,你們感受一下

java.util.GregorianCalendar[  
   time=1467638240540,
   areFieldsSet=true,
   areAllFieldsSet=true,
   lenient=true,
   zone=sun.util.calendar.ZoneInfo   [  
      id="Asia/Shanghai",
      offset=28800000,
      dstSavings=0,
      useDaylight=false,
      transitions=19,
      lastRule=null
   ],
   firstDayOfWeek=1,
   minimalDaysInFirstWeek=1,
   ERA=1,
   YEAR=2016,
   MONTH=6,
   WEEK_OF_YEAR=28,
   WEEK_OF_MONTH=2,
   DAY_OF_MONTH=4,
   DAY_OF_YEAR=186,
   DAY_OF_WEEK=2,
   DAY_OF_WEEK_IN_MONTH=1,
   AM_PM=1,
   HOUR=9,
   HOUR_OF_DAY=21,
   MINUTE=17,
   SECOND=20,
   MILLISECOND=540,
   ZONE_OFFSET=28800000,
   DST_OFFSET=0
]

雖然這從側面反映了Calendar比Date強大,能提供更多的信息,不過估計你們輕易不會使用Calendar的toString()方法的吧?其實在Calendar類中,有一點和Date類是一樣的:它們都會保存一個milliseconds的值。Calendar類對於時間日期的各種計算都是基於這個值來的。你看toString()方法返回的第一個值就是它,足以說明這個值的重要性。

要獲取Calendar對象所提供的信息,需要調用get()方法。在源碼中,get()方法做了如下的事情:

public int get(int field) {
    complete();
    return fields[field];
}

complete()方法中,會調用computeTime()computeFields()方法,這兩個方法都是抽象方法,由子類GregorianCalendar實現。在computeTime()方法中,計算出年月日時分秒等等時間相關的信息,而在computeFields()方法中獲取到對應的時區地區等信息。這兩個方法會將所計算出來的信息保存在一個int類型的數組中,這個數組的名字,沒錯,就是fields。fields數組保存的信息如下所示:

public static final int ERA = 0;
public static final int YEAR = 1;
public static final int MONTH = 2;
public static final int WEEK_OF_YEAR = 3;
public static final int WEEK_OF_MONTH = 4;
public static final int DATE = 5;
public static final int DAY_OF_MONTH = 5;
public static final int DAY_OF_YEAR = 6;
public static final int DAY_OF_WEEK = 7;
public static final int DAY_OF_WEEK_IN_MONTH = 8;
public static final int AM_PM = 9;
public static final int HOUR = 10;
public static final int HOUR_OF_DAY = 11;
public static final int MINUTE = 12;
public static final int SECOND = 13;
public static final int MILLISECOND = 14;
public static final int ZONE_OFFSET = 15;
public static final int DST_OFFSET = 16;

嗯,一共18個常量(注意第6個和第7個的值是一樣的)。也就是說,get()方法中你可以傳入的參數就是上述18個常量之一。你既可以傳常量名(如YEAR等),也可以傳數字進去(如1)。當然爲了可讀性,一般都傳入常量名。其中有幾個參數可能需要解釋一下:

  • ERA 
    表示該日期是在公元紀年(公元0年)之前還是之後,如果返回0表示在公元紀年之前,返回1表示在公元紀年之後。
  • AM_PM 
    表示該時間是在中午12點之前還是之後,如果返回0表示在中午12點之前,返回1表示在中午12點之後。
  • DST_OFFSET 
    表示該時間距夏令時的毫秒數。

下面就舉個簡單的例子來說明一下吧:

public static void main(String[] args) {
    Calendar calendar = Calendar.getInstance();
    System.out.println(calendar.get(Calendar.YEAR));            // 2016
    System.out.println(calendar.get(Calendar.MONTH));           // 6
    System.out.println(calendar.get(Calendar.DAY_OF_MONTH));    // 6
    System.out.println(calendar.get(Calendar.ERA));             // 1
}

有一點要注意的是,由於Calendar類中的常量比較多,你可能傳入的常量名不是上述18個之一,那麼就可能發生一些意想不到的結果,比如,在get()方法中如果我們傳入一個月份,這麼寫:

public static void main(String[] args) {
    Calendar calendar = Calendar.getInstance();
    System.out.println(calendar.get(Calendar.DECEMBER));    // 22
}

返回的22是什麼鬼?其實是因爲Calendar.DECEMBER的值是11,它返回的其實是HOUR_OF_DAY的返回值,也就是22點鐘。用代碼解釋就是下面那樣:

public static void main(String[] args) {
    Calendar calendar = Calendar.getInstance();
    System.out.println(calendar.getTime().toString());      // Mon Jul 04 22:24:08 CST 2016
    System.out.println(calendar.get(Calendar.HOUR_OF_DAY)); // 22
    System.out.println(calendar.get(Calendar.DECEMBER));    // 22
}

如果你不小心在Calendar.get()方法中傳入了類似Calendar.DECEMBER)的參數,那麼編譯器會發出警告,這時候你就可以看看是不是弄錯了。當然,如果你對警告置之不理,那隻能後果自負。

談完了get()方法,該說說set()方法了。get()方法是獲取時間信息,那麼set()方法則是對當前的calendar對象設置時間和日期。那麼我們看看set()方法有那些重載的方法:

public void set(int field, int value) {
    fields[field] = value;
    isSet[field] = true;
    areFieldsSet = isTimeSet = false;
    if (field > MONTH && field < AM_PM) {
        lastDateFieldSet = field;
    }
    if (field == HOUR || field == HOUR_OF_DAY) {
        lastTimeFieldSet = field;
    }
    if (field == AM_PM) {
        lastTimeFieldSet = HOUR;
    }
}
public final void set(int year, int month, int day) {
    set(YEAR, year);
    set(MONTH, month);
    set(DATE, day);
}
public final void set(int year, int month, int day, int hourOfDay, int minute) {
    set(year, month, day);
    set(HOUR_OF_DAY, hourOfDay);
    set(MINUTE, minute);
}
public final void set(int year, int month, int day, int hourOfDay, int minute, int second) {
    set(year, month, day, hourOfDay, minute);
    set(SECOND, second);
}

很簡單,就是將我們傳入的值存放到fields數組中。調用set()方法會改變當前Calendar對象的時間和日期。下面用一段簡單的代碼說明用法:

public static void main(String[] args) {
    Calendar calendar = Calendar.getInstance();

    calendar.set(Calendar.MONTH, 5);
    System.out.println(calendar.get(Calendar.MONTH));           // 5

    calendar.set(2016, Calendar.JANUARY, 2);
    System.out.println(calendar.get(Calendar.YEAR));            // 2016
    System.out.println(calendar.get(Calendar.MONTH));           // 0
    System.out.println(calendar.get(Calendar.DAY_OF_MONTH));    // 2

}

add()和roll()方法

使用get()和set()方法可以獲取當前時間和日期,也可以設置Calendar對象的時間和日期。但是一些簡單的時間日期的計算我們仍然無法快速獲得。比如昨天的日期?這時,就需要使用到add()和roll()方法了。

add(int field, int value)方法接受兩個參數,第一個參數表示你想改變的字段,這個字段是fields數組中的一個。第二個參數是改變的值。下面用代碼看看效果:

public static void main(String[] args) {
    Calendar calendar = Calendar.getInstance();

    System.out.println(calendar.getTime().toString());  // Wed Jul 06 10:41:10 CST 2016
    calendar.add(Calendar.MONTH, 2);
    System.out.println(calendar.get(Calendar.MONTH));   // 8

    calendar.set(Calendar.MONTH, 9);
    System.out.println(calendar.get(Calendar.MONTH));   // 9
    calendar.add(Calendar.MONTH, 4);
    System.out.println(calendar.get(Calendar.MONTH));   // 1

    calendar.set(2016, Calendar.FEBRUARY, 28);
    calendar.add(Calendar.DAY_OF_MONTH, 3);
    System.out.println(calendar.getTime().toString());  //Wed Mar 02 13:18:02 CST 2016

    calendar.set(2016, Calendar.JANUARY, 2);
    calendar.add(Calendar.DAY_OF_MONTH, -3);
    System.out.println(calendar.getTime().toString());  // Wed Dec 30 13:18:02 CST 2015
}

上面這段代碼看懂了嗎?第4行代碼打印了當前時間,當前是7月,第5行代碼將月份增加了2個月,第6行代碼打印了增加之後的月份,嗯?八月?其實不是的。由於在Calendar中月份計數是從0開始的,一月是0,那麼8代表的就是九月。同理,第8行將月份設置的是10月,將月份增加4個月後,就是第二年的2月,也就是返回的1。

第14行代碼,將二月份的日期加三天,則變成了三月二日(2016年2月有29天)。第18行代碼,將1月2日減三天,變成了上一年的12月30日。

由於月份是從0開始計算的,所以計算月份的時候需要特別小心。這就是Calendar反人類的地方之一。

總結一下,使用add()方法有幾點需要注意的:

  • 月份是從0開始計數的。
  • add()方法的第二個參數如果爲正,則數據增加;如果爲負,則數據減少。
  • 當增加的數目超過法定額度時,會自動向更高一級的單位加1。反之亦然。

然後我們再來看看roll()方法。將上面的代碼中的add()方法全部替換爲roll()方法,結果如下:

public static void main(String[] args) {
    Calendar calendar = Calendar.getInstance();

    System.out.println(calendar.getTime().toString());  // Wed Jul 06 13:24:10 CST 2016
    calendar.roll(Calendar.MONTH, 2);
    System.out.println(calendar.get(Calendar.MONTH));   // 8

    calendar.set(Calendar.MONTH, 9);
    System.out.println(calendar.get(Calendar.MONTH));   // 9
    calendar.roll(Calendar.MONTH, 4);
    System.out.println(calendar.get(Calendar.MONTH));   // 1

    calendar.set(2016, Calendar.FEBRUARY, 28);
    calendar.roll(Calendar.DAY_OF_MONTH, 3);
    System.out.println(calendar.getTime().toString());  // Tue Feb 02 13:24:10 CST 2016

    calendar.set(2016, Calendar.JANUARY, 2);
    calendar.roll(Calendar.DAY_OF_MONTH, -3);
    System.out.println(calendar.getTime().toString());  // Sat Jan 30 13:24:10 CST 2016
}

前面的沒問題,和add()效果時一樣的,後面涉及到進位和退位的時候,就和add()方法表現得不一樣了。在roll()方法中不會進行進位和退位運算。這一點是和add()方法最主要的區別。

其它幾個有趣的方法

上面介紹的4個方法,set(),get(),add(),roll()是日常使用頻率比較高的。但是有幾個方法,其實還是比較有趣的,在特定的場合能減少你的代碼量。

1.getMaximum(int field)getMinimum(int field) 
這兩個方法,返回Calendar對象的fields數組中對應數據的最大值和最小值。也就是說,如果我想知道Calendar.DAY_OF_MONTH這個字段能取到的最大值是多少,就可以使用calendar.getMaximum(Calendar.DAY_OF_MONTH)來獲取,返回的值是31,因爲一個月最多可以有31天。

2.getActualMaximum(int field)getActualMinimum(int field) 
這兩個方法名字同上面兩個方法相比,多了一個Actual單詞,表示返回Calendar對象的fields數組中,在當前日期的環境條件下,對應數據的最大值和最小值。也就是說,如果Calendar對象表示的是2016年的2月,那麼使用calendar.getActualMaximum(Calendar.DAY_OF_MONTH)返回的就是29,因爲2016年的2月最多隻有29天。

上面的方法,用代碼解釋如下:

public static void main(String[] args) {
    Calendar calendar = Calendar.getInstance();
    System.out.println(calendar.getMaximum(Calendar.MONTH));              // 11
    System.out.println(calendar.getMinimum(Calendar.HOUR_OF_DAY));        // 0
    System.out.println(calendar.getActualMaximum(Calendar.DATE));         // 31
    System.out.println(calendar.getActualMinimum(Calendar.DATE));         // 1
}

這幾個方法有什麼用呢?比如需要讓用戶輸入本月的一個日期的話,就可以使用getActualMaximum(int field)方法來進行輸入驗證,判斷用戶輸入的日期是否合法。

那麼fields數組中的各個字段的最大值和最小值分別是多少呢?下面是從源碼中找出來的,自己可以對照着上面的fields數組字段名稱欣賞一下。

private static int[] maximums = new int[] { 1, 292278994, 11, 53, 6, 31,
        366, 7, 6, 1, 11, 23, 59, 59, 999, 14 * 3600 * 1000, 7200000 };

private static int[] minimums = new int[] { 0, 1, 0, 1, 0, 1, 1, 1, 1, 0,
        0, 0, 0, 0, 0, -13 * 3600 * 1000, 0 };

和Date類的關係

做爲對Date類的補充和替代,當然得和Date進行相互轉化。

1.使用getTime()方法,從Calendar對象中獲取Date對象。

public static void main(String[] args) {
    Calendar calendar = Calendar.getInstance();
    Date date = calendar.getTime();
    System.out.println(date.toString());            // Mon Jul 04 21:08:27 CST 2016
}

2.使用setTime()方法,將Date對象得值賦值給Calendar對象。

public static void main(String[] args) {
    Calendar calendar = Calendar.getInstance();
    System.out.println(calendar.getTimeInMillis());     // 1467637991365

    Date date = new Date(3600 * 1000);
    calendar.setTime(date);
    System.out.println(calendar.getTimeInMillis());     // 3600000
}

還記得介紹Date類時說過,Date的構造方法中,有4個構造方法已經過時了。其實它們是被Calendar類的相關方法給替代了。比如:

  • Date(int year, int month, int date) 
    Calendar.set(year + 1900, month, date)代替。
  • Date(int year, int month, int date, int hrs, int min) 
    Calendar.set(year + 1900, month, date, hrs, min)代替
  • Date(int year, int month, int date, int hrs, int min, int sec) 
    Calendar.set(year + 1900, month, date, hrs, min, sec)代替
  • Date(String s) 
    DateFormat.parse(String s)代替

 

3. java.text.SimpleDateFormat

有了Date和Calendar,我們能獲取和設置時間和日期,還可以使用SimpleDateFormat這個類來 格式化日期和時間,返回的字符串就是格式化後的結果

比如想要獲得當前時間,輸出是哪年哪月幾號星期幾,幾時幾分幾秒,就可以用下面的代碼來實現:

public static void main(String[] args) {
    Calendar calendar = Calendar.getInstance();
		
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    String dateStr = sdf.format(System.currentTimeMillis());
    System.out.println(dateStr);    //2018-07-21 15:36:55

    String dateStr2 = sdf.format(calendar.getTimeInMillis());
    System.out.println(dateStr2);   //2018-07-21 15:36:55
}

先看一下構造方法,SimpleDateFormat類的構造方法有四個,分別是:

  • SimpleDateFormat()
  • SimpleDateFormat(String pattern)
  • SimpleDateFormat(String pattern, DateFormatSymbols formatSymbols)
  • SimpleDateFormat(String pattern, Locale locale)

如果SimpleDateFormat使用的是空參數構造方法,那需要調用applyPattern(String pattern)方法來將pattern傳入

SimpleDateFormat sdf = new SimpleDateFormat();
String pattern = "2018-07-21 15:36:55";
sdf.applyPattern(pattern);
		
String dateStr = sdf.format(System.currentTimeMillis());
System.out.println(dateStr);

// 與上面一段代碼的效果相同
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dateStr = sdf.format(System.currentTimeMillis());

得到了pattern參數後,SimpleDateFormat對象就具有了格式化日期的能力了,這時只需要調用format()方法,將Date對象傳入該方法中,返回的字符串就是格式化後的結果

pattern參數 官網API文檔。

Letter Date or Time Component Presentation Examples
G Era designator Text AD
y Year Year 1996; 96
Y Week year Year 2009; 09
M Month in year (context sensitive) Month July; Jul; 07
L Month in year (standalone form) Month July; Jul; 07
w Week in year Number 27
W Week in month Number 2
D Day in year Number 189
d Day in month Number 10
F Day of week in month Number 2
E Day name in week Text Tuesday; Tue
u Day number of week (1 = Monday, …, 7 = Sunday) Number 1
a Am/pm marker Text PM
H Hour in day (0-23) Number 0
k Hour in day (1-24) Number 24
K Hour in am/pm (0-11) Number 0
h Hour in am/pm (1-12) Number 12
m Minute in hour Number 30
s Second in minute Number 55
S Millisecond Number 978
z Time zone General time zone Pacific Standard Time; PST; GMT-08:00
Z Time zone RFC 822 time zone -0800
X Time zone ISO 8601 time zone -08; -0800; -08:00

注意區分大小寫

 

SimpleDateFormat類除了能將Date對象進行格式化返回字符串,也能將一個日期字符串解析返回成一個Date對象,比如:

public static void main(String[] args) {
    String s = "2016#08#01";
    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy#MM#dd");
    try {
        Date date = simpleDateFormat.parse(s);
        System.out.println(date.toString());        // Mon Aug 01 00:00:00 CST 2016
        System.out.println(date.getTime());        // 1467637991365
    } catch (ParseException e) {
        e.printStackTrace();
    }
}

嗯,估計大家看出來了,這裏需要使用 # 符號來進行分割數據,其他 - 符號也可以 兩者一致即可

最後說一點,SimpleDateFormat類不是線程安全的,如果有多個線程需要使用到SimpleDateFormat對象,建議每個線程單獨創建,如果多個線程要獲取同一個SimpleDateFormat對象,記得要加鎖!

 

時間的比較:轉換成 long 類型 比較  ,可以得到日期相差的時間

 

參考文章: https://blog.csdn.net/wl9739/article/details/51839502

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章