Java編程的邏輯 (32) - 剖析日期和時間

本系列文章經補充和完善,已修訂整理成書《Java編程的邏輯》,由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買,京東自營連接http://item.jd.com/12299018.htmlhtml


本節和下節,咱們討論在Java中如何進行日期和時間相關的操做。java

日期和時間是一個比較複雜的概念,Java API中對它的支持不是特別好,有一個第三方的類庫反而特別受歡迎,這個類庫是Joda-Time,Java 1.8受Joda-Time影響,從新設計了日期和時間API,新增了一個包java.time。算法

雖然以前的設計有一些不足,但Java API依然是被大量使用的,本節介紹Java 1.8以前API中對日期和時間的支持,下節介紹Joda-Time,Java 1.8中的新API與Joda-Time比較相似,暫時就不介紹了。編程

關於日期和時間,有一些基本概念,咱們先來看下。設計模式

基本概念數組

時區安全

咱們都知道,同一時刻,世界上各個地區的時間多是不同的,具體時間與時區有關,一共有24個時區,英國格林尼治是0時區,北京是東八區,也就是說格林尼治凌晨1點,北京是早上9點。0時區的時間也稱爲GMT+0時間,GMT是格林尼治標準時間,北京的時間就是GMT+8:00。微信

時刻和Epoch Time (紀元時)多線程

全部計算機系統內部都用一個整數表示時刻,這個整數是距離格林尼治標準時間1970年1月1日0時0分0秒的毫秒數。爲何要用這個時間呢?更多的是歷史緣由,本文就不介紹了。this

格林尼治標準時間1970年1月1日0時0分0秒也被稱爲Epoch Time (紀元時)。

這個整數表示的是一個時刻,與時區無關,世界上各個地方都是同一個時刻,但各個地區對這個時刻的解讀,如年月日時分秒,多是不同的。

如何表示1970年之前的時間呢?使用負數。

年曆

咱們都知道,中國有公曆和農曆之分,公曆和農曆都是年曆,不一樣的年曆,一年有多少月,每個月有多少天,甚至一天有多少小時,這些可能都是不同的。

好比,公曆有閏年,閏年2月是29天,而其餘年份則是28天,其餘月份,有的是30天,有的是31天。農曆有閏月,好比閏7月,一年就會有兩個7月,一共13個月。

公曆是世界上普遍採用的年曆,除了公曆,還有其餘一些年曆,好比日本也有本身的年曆。Java API的設計思想是支持國際化的,支持多種年曆,但實際中沒有直接支持中國的農曆,本文主要討論公曆。

簡單總結下,時刻是一個絕對時間,對時刻的解讀,如年月日周時分秒等,則是相對的,與年曆和時區相關。

Java日期和時間API

Java API中關於日期和時間,有三個主要的類:

  • Date:表示時刻,即絕對時間,與年月日無關。
  • Calendar:表示年曆,Calendar是一個抽象類,其中表示公曆的子類是GregorianCalendar
  • DateFormat:表示格式化,可以將日期和時間與字符串進行相互轉換,DateFormat也是一個抽象類,其中最經常使用的子類是SimpleDateFormat。 

還有兩個相關的類:

  • TimeZone: 表示時區
  • Locale: 表示國家和語言 

下面,咱們來看這些類。

Date

Date是Java API中最先引入的關於日期的類,一開始,Date也承載了關於年曆的角色,但因爲不能支持國際化,其中的不少方法都已通過時了,被標記爲了@Deprecated,再也不建議使用。

Date表示時刻,內部主要是一個long類型的值,以下所示:

private transient long fastTime;

fastTime表示距離紀元時的毫秒數,此處,關於transient關鍵字,咱們暫時忽略。

Date有兩個構造方法:

public Date(long date) {
    fastTime = date;
}

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

第一個構造方法,就是根據傳入的毫秒數進行初始化,第二個構造方法是默認構造方法,它根據System.currentTimeMillis()的返回值進行初始化。System.currentTimeMillis()是一個經常使用的方法,它返回當前時刻距離紀元時的毫秒數。

Date中的大部分方法都已通過時了,其中沒有過期的主要方法有:

返回毫秒數

public long getTime() 

判斷與其餘Date是否相同

public boolean equals(Object obj)

主要就是比較內部的毫秒數是否相同。

與其餘Date進行比較

public int compareTo(Date anotherDate)

Date實現了Comparable接口,比較也是比較內部的毫秒數,若是當前Date的毫秒數小於參數中的,返回-1,相同返回0,不然返回1。

除了compareTo,還有另外兩個方法,與給定日期比較,判斷是否在給定日期以前或以後,內部比較的也是毫秒數。

public boolean before(Date when)
public boolean after(Date when)

哈希值

public int hashCode()

哈希值算法與Long相似。

TimeZone

TimeZone表示時區,它是一個抽象類,有靜態方法用於獲取其實例。

獲取當前的默認時區,代碼爲:

TimeZone tz = TimeZone.getDefault();
System.out.println(tz.getID());

獲取默認時區,並輸出其ID,在個人電腦上,輸出爲:

Asia/Shanghai

默認時區是在哪裏設置的呢,能夠更改嗎?Java中有一個系統屬性,user.timezone,保存的就是默認時區,系統屬性能夠經過System.getProperty得到,以下所示:

System.out.println(System.getProperty("user.timezone"));

在個人電腦上,輸出爲:

Asia/Shanghai

系統屬性能夠在Java啓動的時候傳入參數進行更改,如

java -Duser.timezone=Asia/Shanghai xxxx

TimeZone也有靜態方法,能夠得到任意給定時區的實例,好比:

獲取美國東部時區

TimeZone tz = TimeZone.getTimeZone("US/Eastern");

ID除了能夠是名稱外,還能夠是GMT形式表示的時區,如:

TimeZone tz = TimeZone.getTimeZone("GMT+08:00");

國家和語言Locale

Locale表示國家和語言,它有兩個主要參數,一個是國家,另外一個是語言,每一個參數都有一個代碼,不過國家並非必須的。

好比說,中國的大陸代碼是CN,臺灣地區的代碼是TW,美國的代碼是US,中文語言的代碼是zh,英文是en。

Locale類中定義了一些靜態變量,表示常見的Locale,好比:

  • Locale.US:表示美國英語
  • Locale.ENGLISH:表示全部英語
  • Locale.TAIWAN:表示臺灣中文
  • Locale.CHINESE:表示全部中文
  • Locale.SIMPLIFIED_CHINESE:表示大陸中文

與TimeZone相似,Locale也有靜態方法獲取默認值,如:

Locale locale = Locale.getDefault();
System.out.println(locale.toString());

在個人電腦上,輸出爲:

zh_CN

Calendar

Calendar類是日期和時間操做中的主要類,它表示與TimeZone和Locale相關的日曆信息,能夠進行各類相關的運算。

咱們先來看下它的內部組成。

內部組成

與Date相似,Calendar內部也有一個表示時刻的毫秒數,定義爲:

protected long  time;

除此以外,Calendar內部還有一個數組,表示日曆中各個字段的值,定義爲:

protected int   fields[];

這個數組的長度爲17,保存一個日期中各個字段的值,都有哪些字段呢?Calendar類中定義了一些靜態變量,表示這些字段,主要有:

  • Calendar.YEAR:表示年
  • Calendar.MONTH:表示月,一月份是0,Calendar一樣定義了表示各個月份的靜態變量,如Calendar.JULY表示7月。
  • Calendar.DAY_OF_MONTH:表示日,每個月的第一天是1。
  • Calendar.HOUR_OF_DAY:表示小時,從0到23。
  • Calendar.MINUTE:表示分鐘,0到59。
  • Calendar.SECOND:表示秒,0到59。
  • Calendar.MILLISECOND:表示毫秒,0到999。
  • Calendar.DAY_OF_WEEK:表示星期幾,週日是1,週一是2,週六是7,Calenar一樣定義了表示各個星期的靜態變量,如Calendar.SUNDAY表示週日。 

獲取Calendar實例

Calendar是抽象類,不能直接建立對象,它提供了四個靜態方法,能夠獲取Calendar實例,分別爲:

public static Calendar getInstance()
public static Calendar getInstance(Locale aLocale)
public static Calendar getInstance(TimeZone zone)
public static Calendar getInstance(TimeZone zone, Locale aLocale)

最終調用的方法都是須要TimeZone和Locale的,若是沒有,則會使用上面介紹的默認值。getInstance方法會根據TimeZone和Locale建立對應的Calendar子類對象,在中文系統中,子類通常是表示公曆的GregorianCalendar。

getInstance方法封裝了Calendar對象建立的細節,TimeZone和Locale不一樣,具體的子類可能不一樣,但都是Calendar,這種隱藏對象建立細節的方式,是計算機程序中一種常見的設計模式,它有一個名字,叫工廠方法,getInstance就是一個工廠方法,它生產對象。

獲取日曆信息

與new Date()相似,新建立的Calendar對象表示的也是當前時間,與Date不一樣的是,Calendar對象能夠方便的獲取年月日等日曆信息。

來看代碼,輸出當前時間的各類信息:

Calendar calendar = Calendar.getInstance();
System.out.println("year: "+calendar.get(Calendar.YEAR));
System.out.println("month: "+calendar.get(Calendar.MONTH));
System.out.println("day: "+calendar.get(Calendar.DAY_OF_MONTH));
System.out.println("hour: "+calendar.get(Calendar.HOUR_OF_DAY));
System.out.println("minute: "+calendar.get(Calendar.MINUTE));
System.out.println("second: "+calendar.get(Calendar.SECOND));
System.out.println("millisecond: " +calendar.get(Calendar.MILLISECOND));
System.out.println("day_of_week: " + calendar.get(Calendar.DAY_OF_WEEK));

具體輸出與執行時的時間和默認的TimeZone以及Locale有關,在寫做時,個人電腦上的輸出爲:

year: 2016
month: 7
day: 14
hour: 13
minute: 55
second: 51
millisecond: 564
day_of_week: 2

內部,Calendar會將表示時刻的毫秒數,按照TimeZone和Locale對應的年曆,計算各個日曆字段的值,存放在fields數組中,Calendar.get方法獲取的就是fields數組中對應字段的值。

設置和修改時間

Calendar支持根據Date或毫秒數設置時間:

public final void setTime(Date date)
public void setTimeInMillis(long millis)

也支持根據年月日等日曆字段設置時間:

public final void set(int year, int month, int date)
public final void set(int year, int month, int date, int hourOfDay, int minute)
public final void set(int year, int month, int date, int hourOfDay, int minute, int second)
public void set(int field, int value)

除了直接設置,Calendar支持根據字段增長和減小時間:

public void add(int field, int amount)

amount爲正數表示增長,負數表示減小。

好比說,若是想設置Calendar爲次日的下午2點15,代碼能夠爲:

Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DAY_OF_MONTH, 1);
calendar.set(Calendar.HOUR_OF_DAY, 14);
calendar.set(Calendar.MINUTE, 15);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);

Calendar的這些方法中一個比較方便和強大的地方在於,它可以自動調整相關的字段。

好比說,咱們知道二月份最多有29天,若是當前時間爲1月30號,對Calendar.MONTH字段加1,即增長一月,Calendar不是簡單的只對月字段加1,那樣日期是2月30號,是無效的,Calendar會自動調整爲2月最後一天,即2月28或29。

再好比,設置的值能夠超出其字段最大範圍,Calendar會自動更新其餘字段,如:

Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.HOUR_OF_DAY, 48);
calendar.add(Calendar.MINUTE, -120);

至關於增長了46小時。

內部,根據字段設置或修改時間時,Calendar會更新fields數組對應字段的值,但通常不會當即更新其餘相關字段或內部的毫秒數的值,不過在獲取時間或字段值的時候,Calendar會從新計算並更新相關字段。

簡單總結下,Calenar作了一項很是繁瑣的工做,根據TimeZone和Locale,在絕對時間毫秒數和日曆字段之間自動進行轉換,且對不一樣日曆字段的修改進行自動同步更新。

除了add,Calendar還有一個相似的方法:

public void roll(int field, int amount)

與add的區別是,這個方法不影響時間範圍更大的字段值。好比說:

Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.HOUR_OF_DAY, 13);
calendar.set(Calendar.MINUTE, 59);
calendar.add(Calendar.MINUTE, 3);

calendar首先設置爲13:59,而後分鐘字段加3,執行後的calendar時間爲14:02。若是add改成roll,即:

calendar.roll(Calendar.MINUTE, 3);

則執行後的calendar時間會變爲13:02,在分鐘字段上執行roll不會改變小時的值。

轉換爲Date或毫秒數

Calendar能夠方便的轉換爲Date或毫秒數,方法是:

public final Date getTime()
public long getTimeInMillis() 

Calendar的比較

與Date相似,Calendar之間也能夠進行比較,也實現了Comparable接口,相關方法有:

public boolean equals(Object obj)
public int compareTo(Calendar anotherCalendar)
public boolean after(Object when)
public boolean before(Object when)

DateFormat

DateFormat類主要在Date和字符串表示之間進行相互轉換,它有兩個主要的方法:

public final String format(Date date)
public Date parse(String source)

format將Date轉換爲字符串,parse將字符串轉換爲Date。

Date的字符串表示與TimeZone和Locale都是相關的,除此以外,還與兩個格式化風格有關,一個是日期的格式化風格,另外一個是時間的格式化風格。

DateFormat定義了四個靜態變量,表示四種風格,SHORT、MEDIUM、LONG和FULL,還定義了一個靜態變量DEFAULT,表示默認風格,值爲MEDIUM,不一樣風格輸出的信息詳細程度不一樣。

與Calendar相似,DateFormat也是抽象類,也用工廠模式建立對象,提供了多個靜態方法建立DateFormat對象,有三類方法:

public final static DateFormat getDateTimeInstance()
public final static DateFormat getDateInstance()
public final static DateFormat getTimeInstance()

getDateTimeInstance既處理日期也處理時間,getDateInstance只處理日期,getTimeInstance只處理時間,看下面代碼:

Calendar calendar = Calendar.getInstance();
//2016-08-15 14:15:20
calendar.set(2016, 07, 15, 14, 15, 20);
System.out.println(DateFormat.getDateTimeInstance()
        .format(calendar.getTime()));
System.out.println(DateFormat.getDateInstance()
        .format(calendar.getTime()));
System.out.println(DateFormat.getTimeInstance()
        .format(calendar.getTime()));

輸出爲:

2016-8-15 14:15:20
2016-8-15
14:15:20

每類工廠方法都有兩個重載的方法,接受日期和時間風格以及Locale做爲參數:

DateFormat getDateTimeInstance(int dateStyle, int timeStyle)
DateFormat getDateTimeInstance(int dateStyle, int timeStyle, Locale aLocale)

好比,看下面代碼:

Calendar calendar = Calendar.getInstance();
//2016-08-15 14:15:20
calendar.set(2016, 07, 15, 14, 15, 20);
System.out.println(DateFormat.getDateTimeInstance(
        DateFormat.LONG,DateFormat.SHORT,Locale.CHINESE)
        .format(calendar.getTime()));

輸出爲:

2016年8月15日 下午2:15

DateFormat的工廠方法裏,咱們沒看到TimeZone參數,不過,DateFormat提供了一個setter方法,能夠設置TimeZone:

public void setTimeZone(TimeZone zone)

DateFormat雖然比較方便,但若是咱們要對字符串格式有更精確的控制,應該使用SimpleDateFormat這個類。

SimpleDateFormat

SimpleDateFormat是DateFormat的子類,相比DateFormat,它的一個主要不一樣是,它能夠接受一個自定義的模式(pattern)做爲參數,這個模式規定了Date的字符串形式。先看個例子:

Calendar calendar = Calendar.getInstance();
//2016-08-15 14:15:20
calendar.set(2016, 07, 15, 14, 15, 20);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 E HH時mm分ss秒");
System.out.println(sdf.format(calendar.getTime()));

輸出爲:

2016年08月15日 星期一 14時15分20秒 

SimpleDateFormat有個構造方法,能夠接受一個pattern做爲參數,這裏pattern是:

yyyy年MM月dd日 E HH時mm分ss秒

pattern中的英文字符a-z和A-Z表示特殊含義,其餘字符原樣輸出,這裏:

  • yyyy:表示四位的年
  • MM:表示月,兩位數表示
  • dd:表示日,兩位數表示
  • HH:表示24小時制的小時數,兩位數表示
  • mm:表示分鐘,兩位數表示
  • ss:表示秒,兩位數表示
  • E:表示星期幾 

這裏須要特地提醒一下,hh也表示小時數,但表示的是12小時制的小時數,而a表示的是上午仍是下午,看代碼:

Calendar calendar = Calendar.getInstance();
//2016-08-15 14:15:20
calendar.set(2016, 07, 15, 14, 15, 20);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd hh:mm:ss a");
System.out.println(sdf.format(calendar.getTime()));

輸出爲:

2016/08/15 02:15:20 下午

更多的特殊含義能夠參看SimpleDateFormat的Java文檔。若是想原樣輸出英文字符,能夠用單引號括起來。

除了將Date轉換爲字符串,SimpleDateFormat也能夠方便的將字符轉化爲Date,看代碼:

String str = "2016-08-15 14:15:20.456";
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
try {
    Date date = sdf.parse(str);
    SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy年M月d h:m:s.S a");
    System.out.println(sdf2.format(date));
} catch (ParseException e) {
    e.printStackTrace();
}

輸出爲:

2016年8月15 2:15:20.456 下午

代碼將字符串解析爲了一個Date對象,而後使用另一個格式進行了輸出,這裏SSS表示三位的毫秒數。

須要注意的是,parse會拋出一個受檢異常(checked exception),異常類型爲ParseException,調用者必須進行處理。

侷限性

至此,關於Java 1.8以前的日期和時間相關API的主要內容,咱們就介紹的差很少了,這裏咱們想強調一下這些API的一些侷限性。

Date中的過期方法

Date中的方法參數與常識不符合,過期方法標記容易被人忽略,產生誤用。好比說,看以下代碼:

Date date = new Date(2016,8,15);
System.out.println(DateFormat.getDateInstance().format(date));

想固然的輸出爲2016-08-15,但其實輸出爲:

3916-9-15

之因此產生這個輸出,是由於,Date構造方法中的year表示的是與1900年的差,month是從0開始的。

Calendar操做比較囉嗦臃腫

Calendar API的設計不是很成功,一些簡單的操做都須要屢次方法調用,寫不少代碼,比較囉嗦臃腫。

另外,Calendar難以進行比較複雜的日期操做,好比,計算兩個日期之間有多少個月,根據生日計算年齡,計算下個月的第一個週一等。

下一節,咱們會介紹Joda-Time,相比Calendar,Joda-Time要簡潔方便的多。

DateFormat的線程安全性

DateFormat/SimpleDateFormat不是線程安全的,關於線程概念,後續文章咱們會詳解,這裏簡單說明一下,多個線程同時使用一個DateFormat實例的時候,會有問題,由於DateFormat內部使用了一個Calendar實例對象,多線程同時調用的時候,這個Calendar實例的狀態可能就會紊亂。

解決這個問題大概有如下方案:

  • 每次使用DateFormat都新建一個對象
  • 使用線程同步
  • 使用ThreadLocal
  • 使用Joda-Time,Joda-Time是線程安全的

後續文章咱們再介紹線程同步和ThreadLocal。

小結

本節介紹了Java中(1.8以前)的日期和時間相關API,Date表示時刻,與年月日無關,Calendar表示日曆,與時區和Locale相關,可進行各類運算,是日期時間操做的主要類,DateFormat/SimpleDateFormat在Date和字符串之間進行相互轉換。

這些API存在着一些不足,操做比較複雜,代碼比較臃腫,還有線程安全的問題,實際中一個經常使用的第三方庫是Joda-Time,下一節,讓咱們一塊兒來看下。

----------------

未完待續,查看最新文章,敬請關注微信公衆號「老馬說編程」(掃描下方二維碼),從入門到高級,深刻淺出,老馬和你一塊兒探索Java編程及計算機技術的本質。用心寫做,原創文章,保留全部版權。

相關文章
相關標籤/搜索