自從14 年發布Java 8 以後,我們古老java.util.Date 終於不再是我們Java 裡操作日期時間的唯一的選擇。
其實Java 裡的日期時間的相關API 一直為世猿詬病,不僅在於它設計分上工不明確,往往一個類既能處理日期又能處理時間,很混亂,還在於某些年月日期的數值映射存儲反人類,例如:0 對應月份一月,11 對應月份十二月,118 對應年份2018(1900 + 118)等。
往往我們得到某個年月值還需要再做相應的運算才能得到準確的年月日信息,直到我們的Java 8 ,借鑒了第三方開源庫Joda-Time 的優秀設計,重新設計了一個日期時間API,相比之前,可以說好用百倍,相關API 接口全部位於包java.time 下。
古老的日期時間接口
表示時刻信息的Date
世界上所有的計算機內部存儲時間都使用一個long 類型的整數,而這個整數的值就是相對於英國格林尼治標準時間(1970年1月1日0時0分0秒)的毫秒數。例如:
public static void main(String[] args){ //January 1, 1970 00:00:00 GMT. Date date = new Date(1000); System.out.println(date);}輸出結果:
//1970-1-1 8:00:01Thu Jan 01 08:00:01 CST 1970
很多人可能會疑惑,1000 表示的是距離標準時間往後1 秒,那為什麼時間卻多走了八個小時?
這和「時區」有關係,如果你位於英國的格林尼治區,那麼結果會如預想一樣,但是我們位於中國東八區,時間要早八個小時,所以不同時區基於的基礎值不同。
Date 這個類以前真的扮演過很多角色,從它的源碼就可以看出來,有可以操作時刻的方法,有可以操作年月日的方法,甚至它還能管時區。可以說,日期時間的相關操作有它一個人就足夠了。
但這個世界就是這樣,你管的東西多了,自然就不能面面俱到,Date 中很多方法的設計並不是很合理,之前我們也說了,甚至有點反人類。所以,現在的Date 類中接近百分之八十的方法都已廢棄,被標記為@Deprecated。
sun 公司給Date 的目前定位是,唯一表示一個時刻,所以它的內部應該圍繞著那個整型的毫秒,而不再著重於各種年曆時區等信息。
Date 允許通過以下兩種構造器實例化一個對象:
private transient long fastTime;public Date() { this(System.currentTimeMillis());}public Date(long date) { fastTime = date;}這裡的fastTime 屬性存儲的就是時刻所對應的毫秒數,兩個構造器還是很簡單,如果調用的是無參構造器,那麼虛擬機將以系統當前的時刻值對fastTime 進行賦值。
還有幾個為數不多沒有被廢棄的方法:
還有兩個方法是jdk1.8 以後新增的,用於向Java 8 新增接口的轉換,待會介紹。
描述年曆的Calendar
Calendar 用於表示年月日等日期信息,它是一個抽像類,所以一般通過以下四種工廠方法獲取它的實例對象。
public static Calendar getInstance()public static Calendar getInstance(TimeZone zone)public static Calendar getInstance(Locale aLocale)public static Calendar getInstance(TimeZone zone,Locale aLocale)
其實內部最終會調用同一個內部方法:
private static Calendar createCalendar(TimeZone zone,Locale aLocale)
該方法需要兩個參數,一個是時區,一個是國家和語言,也就是說,構建一個Calendar 實例最少需要提供這兩個參數信息,否則將會使用系統默認的時區或語言信息。
因為不同的時區與國家語言對於時刻和年月日信息的輸出是不同的,所以這也是為什麼一個Calendar 實例必須傳入時區和國家信息的一個原因。看個例子:
public static void main(String[] args){ Calendar calendar = Calendar.getInstance(); System.out.println(calendar.getTime()); Calendar calendar1 = Calendar.getInstance (TimeZone.getTimeZone("GMT"), Locale.ENGLISH); System.out.println( calendar1.get(Calendar.YEAR) + ":" + calendar1.get(Calendar.HOUR) + ":" + calendar1.get(Calendar.MINUTE)); }輸出結果:
Sat Apr 21 10:32:20 CST 20182018:2:32
可以看到,第一個輸出為我們系統默認時區與國家的當前時間,而第二個Calendar 實例我們指定了它位於格林尼治時區(0 時區),結果也顯而易見了,相差了八個小時,那是因為我們位於東八區,時間早於0 時區八個小時。
可能有人會疑惑了,為什麼第二個Calendar 實例的輸出要如此復雜的拼接,而不像第一個Calendar 實例那樣直接調用getTime 方法簡潔呢?
這涉及到Calendar 的內部實現,我們一起看看:
protected long time;public final Date getTime() { return new Date(getTimeInMillis());}和Date 一樣,Calendar 的內部也維護著一個時刻信息,而getTime 方法實際上是根據這個時刻構建了一個Date 對象並返回的。
而一般我們構建Calendar 實例的時候都不會傳入一個時刻信息,所以這個time 的值在實例初始化的時候,程序會根據系統默認的時區和當前時間計算得到一個毫秒數並賦值給time。
所以,所有未手動修改time 屬性值的Calendar 實例的內部,time 的值都是當時系統默認時區的時刻數值。也就是說,getTime 的輸出結果是不會理會當前實例所對應的時區信息的,這也是我覺得Calendar 設計的一個缺陷所在,因為這樣會導致兩個不同時區Calendar 實例的getTime 輸出值只取決於實例初始化時系統的運行時刻。
Calendar 中也定義了很多靜態常量和一些屬性數組:
public final static int ERA = 0;public final static int YEAR = 1;public final static int MONTH = 2;public final static int WEEK_OF_YEAR = 3;public final static int WEEK_OF_MONTH = 4;public final static int DATE = 5;....protected int fields[];protected boolean isSet[];...
有關日期的所有相關信息都存儲在屬性數組中,而這些靜態常量的值往往表示的就是一個索引值,通過get 方法,我們傳入一個屬性索引,返回得到該屬性的值。例如:
Calendar myCalendar = Calendar.getInstance();int year = myCalendar.get(Calendar.YEAR);
這裡的get 方法實際上就是直接取的fields[1] 作為返回值,而fields 屬性數組在Calendar 實例初始化的時候就已經由系統根據時區和語言計算並賦值了,注意,這裡會根據你指定的時區進行計算,它不像time 始終是依照的系統默認時區。
個人覺得Calendar 的設計有優雅的地方,也有不合理的地方,畢竟是個「古董」了,終將被替代。
DateFormat 格式化轉換
從我們之前的一個例子中可以看到,Calendar 想要輸出一個預期格式的日期信息是很麻煩的,需要自己手動拼接。而我們的DateFormat 就是用來處理格式化字符串和日期時間之間的轉換操作的。
DateFormat 和Calendar 一樣,也是一個抽像類,我們需要通過工廠方式產生其實例對象,主要有以下幾種工廠方法:
//只處理時間的轉換public final static DateFormat getTimeInstance()//只處理日期的轉換public final static DateFormat getDateInstance()//既可以處理時間,也可以處理日期public final static DateFormat getDateTimeInstance()
當然,它們各自都有各自的重載方法,具體的我們待會兒看。
DateFormat 有兩類方法,format 和parse。
public final String format(Date date)public Date parse(String source)
format 方法用於將一個日期對象格式化為字符串,parse 方法用於將一個格式化的字符串裝換為一個日期對象。例如:
public static void main(String[] args){ Calendar calendar = Calendar.getInstance(); DateFormat dateFormat = DateFormat.getDateTimeInstance(); System.out.println(dateFormat.format(calendar.getTime()));}輸出結果:
2018-4-21 16:58:09
顯然,使用工廠構造的DateFormat 實例並不能夠自定義輸出格式化內容,即輸出的字符串格式是固定的,不能滿足某些情況下的特殊需求。一般我們會直接使用它的一個實現類,SimpleDateFormat。
SimpleDateFormat 允許在構造實例的時候傳入一個pattern 參數,自定義日期字符的輸出格式。例如:
public static void main(String[] args){ DateFormat dateFormat = new SimpleDateFormat("yyyy年MM月dd日"); System.out.println(dateFormat.format(new Date()));}輸出結果:
2018年04月21日
其中,
當然,對於字符串轉日期也是很方便的,允許自定義模式,但必須遵守自己制定的模式,否則程序將無法成功解析。例如:
public static void main(String[] args){ String str = "2018年4月21日17點17分星期六"; DateFormat sDateFormat = new SimpleDateFormat("yyyy年M月dd日HH點mm分E"); sDateFormat.parse(str); System.out.println(sDateFormat.getCalendar().getTime());}輸出結果:
Sat Apr 21 17:17:00 CST 2018
顯然,程序是正確的解析的我們的字符串並轉換為Calendar 對象存儲在DateFormat 內部的。
總的來說,Date、Calendar 和DateFormat 已經能夠處理一般的時間日期問題了,但是不可避免的是,它們依然很繁瑣,不好用。
限於篇幅,我們下篇將對比Java 8 的新式日期時間API,你會發現它更加優雅的設計和簡單的操作性。