日期與時間
日期與時間是計算機處理的重要數據。絕大部分程序的運行都要和時間打交道。本節我們將詳細講解Java程序如何正確處理日期與時間。
基本概念
在計算機中,我們經常需要處理日期和時間。
- 這是日期:
- 2019-11-20
- 2020-1-1
- 這是時間:
- 12:30:59
- 2020-1-1 20:21:59
日期是指某一天,它不是連續變化的,而是應該被看成離散的。而時間有兩種概念,
- 一種是不帶日期的時間,例如,12:30:59。
- 另一種是帶日期的時間,例如,2020-1-1 20:21:59,
只有這種帶日期的時間能唯一確定某個時刻,不帶日期的時間是無法確定一個唯一時刻的。
本地時間:
當我們說當前時刻是2019年11月20日早上8:15的時候,我們說的實際上是本地時間。在國內就是北京時間。在這個時刻,如果地球上不同地方的人們同時看一眼手表,他們各自的本地時間是不同的:
所以,不同的時區,在同一時刻,本地時間是不同的。全球一共分為24個時區,倫敦所在的時區稱為標准時區,其他時區按東/西偏移的小時區分,北京所在的時區是東八區。
時區:
因為光靠本地時間還無法唯一確定一個准確的時刻,所以我們還需要給本地時間加上(這里的加上不是數學上的加,而是給出本地時間和所在的時區,可以表示一個唯一准確的時刻)一個時區。時區有好幾種表示方式。
- 一種是以GMT或者UTC加時區偏移表示,例如:GMT+08:00或者UTC+08:00表示東八區。
GMT和UTC可以認為基本是等價的,只是UTC使用更精確的原子鍾計時,每隔幾年會有一個閏秒,我們在開發程序的時候可以忽略兩者的誤差,因為計算機的時鍾在聯網的時候會自動與時間服務器同步時間。 - 另一種是縮寫,例如,CST表示China Standard Time,也就是中國標准時間。但是CST也可以表示美國中部時間Central Standard Time USA,因此,縮寫容易產生混淆,我們盡量不要使用縮寫。
- 最后一種是以洲/城市表示,例如,Asia/Shanghai,表示上海所在地的時區。特別注意城市名稱不是任意的城市,而是由國際標准組織規定的城市。
因為時區的存在,東八區的2019年11月20日早上8:15,和西五區的2019年11月19日晚上19:15,他們的時刻是相同的:
時刻相同的意思就是,分別在兩個時區的兩個人,如果在這一刻通電話,他們各自報出自己手表上的時間,雖然本地時間是不同的,但是這兩個時間表示的時刻是相同的。
夏令時:
時區還不是最復雜的,更復雜的是夏令時。所謂夏令時,就是夏天開始的時候,把時間往后撥1小時,夏天結束的時候,再把時間往前撥1小時。我們國家實行過一段時間夏令時,1992年就廢除了,但是矯情的美國人到現在還在使用,所以時間換算更加復雜。
因為涉及到夏令時,相同的時區,如果表示的方式不同,轉換出的時間是不同的。我們舉個栗子,對於2019-11-20和2019-6-20兩個日期來說,假設北京人在紐約:
- 如果以GMT或者UTC作為時區,無論日期是多少,時間都是19:00;
- 如果以國家/城市表示,例如America/NewYork,雖然紐約也在西五區,但是,因為夏令時的存在,在不同的日期,GMT時間和紐約時間可能是不一樣的:
實行夏令時的不同地區,進入和退出夏令時的時間很可能是不同的。同一個地區,根據歷史上是否實行過夏令時,標准時間在不同年份換算成當地時間也是不同的。因此,計算夏令時,沒有統一的公式,必須按照一組給定的規則來算,並且,該規則要定期更新。
注:計算夏令時請使用標准庫提供的相關類,不要試圖自己計算夏令時。
本地化:
在計算機中,通常使用Locale表示一個國家或地區的日期、時間、數字、貨幣等格式。Locale由語言_國家的字母縮寫構成,例如,zh_CN表示中文+中國,en_US表示英文+美國。語言使用小寫,國家使用大寫。
對於日期來說,不同的Locale,例如,中國和美國的表示方式如下:
- zh_CN:2016-11-30
- en_US:11/30/2016
計算機用Locale在日期、時間、貨幣和字符串之間進行轉換。一個電商網站會根據用戶所在的Locale對用戶顯示如下:
小結:
- 在編寫日期和時間的程序前,我們要准確理解日期、時間和時刻的概念。
- 由於存在本地時間,我們需要理解時區的概念,並且必須牢記由於夏令時的存在,同一地區用GMT/UTC和城市表示的時區可能導致時間不同。
- 計算機通過Locale來針對當地用戶習慣格式化日期、時間、數字、貨幣等。
Date和Calendar
在理解日期和時間的表示方式之前,我們先要理解數據的存儲和展示。
當我們定義一個整型變量並賦值時:int n = 123400;
編譯器會把上述字符串(程序源碼就是一個字符串)編譯成字節碼。在程序的運行期,變量n指向的內存實際上是一個4字節區域:
注意到計算機內存除了二進制的0/1外沒有其他任何格式。上述十六機制是為了簡化表示。
當我們用System.out.println(n)打印這個整數的時候,實際上println()這個方法在內部把int類型轉換成String類型,然后打印出字符串123400。
類似的,我們也可以以十六進制的形式打印這個整數,或者,如果n表示一個價格,我們就以$123,400.00的形式來打印它:
可見,整數123400是數據的存儲格式,它的存儲格式非常簡單。而我們打印的各種各樣的字符串,則是數據的展示格式。展示格式有多種形式,但本質上它就是一個轉換方法:String toDisplay(int n) { ... }
理解了數據的存儲和展示,我們回頭看看以下幾種日期和時間:
- 2019-11-20 0:15:01 GMT+00:00
- 2019年11月20日8:15:01
- 11/19/2019 19:15:01 America/New_York
它們實際上是數據的展示格式,分別按英國時區、中國時區、紐約時區對同一個時刻進行展示。而這個“同一個時刻”在計算機中存儲的本質上只是一個整數,我們稱它為Epoch Time。Epoch Time是計算從1970年1月1日零點(格林威治時區/GMT+00:00)到現在所經歷的秒數,例如:
1574208900
表示從從1970年1月1日零點GMT時區到該時刻一共經歷了1574208900秒,換算成倫敦、北京和紐約時間分別是:
因此,在計算機中,只需要存儲一個整數1574208900表示某一時刻。當需要顯示為某一地區的當地時間時,我們就把它格式化為一個字符串:String displayDateTime(int n, String timezone) { ... }
Epoch Time又稱為時間戳,在不同的編程語言中,會有幾種存儲方式:
- 以秒為單位的整數:1574208900,缺點是精度只能到秒;
- 以毫秒為單位的整數:1574208900123,最后3位表示毫秒數;
- 以秒為單位的浮點數:1574208900.123,小數點后面表示零點幾秒。
它們之間轉換非常簡單。而在Java程序中,時間戳通常是用long表示的毫秒數,即:long t = 1574208900123L;
轉換成北京時間就是2019-11-20T8:15:00.123
。要獲取當前時間戳,可以使用System.currentTimeMillis(),這是Java程序獲取時間戳最常用的方法。
標准庫API:
我們再來看一下Java標准庫提供的API。Java標准庫有兩套處理日期和時間的API:
- 一套定義在java.util這個包里面,主要包括Date、Calendar和TimeZone這幾個類;
- 一套新的API是在Java 8引入的,定義在java.time這個包里面,主要包括LocalDateTime、ZonedDateTime、ZoneId等。
為什么會有新舊兩套API呢?因為歷史遺留原因,舊的API存在很多問題,所以引入了新的API。
那么我們能不能跳過舊的API直接用新的API呢?如果涉及到遺留代碼就不行,因為很多遺留代碼仍然使用舊的API,所以目前仍然需要對舊的API有一定了解,很多時候還需要在新舊兩種對象之間進行轉換。
本節我們快速講解舊API的常用類型和方法。
Date:
java.util.Date是用於表示一個日期和時間的對象,注意與java.sql.Date區分,后者用在數據庫中。如果觀察Date的源碼,可以發現它實際上存儲了一個long類型的以毫秒表示的時間戳:
public class Date implements Serializable, Cloneable, Comparable<Date> {
private transient long fastTime;
...
}
我們來看Date的基本用法:
注意getYear()返回的年份必須加上1900,getMonth()返回的月份是011分別表示112月,所以要加1,而getDate()返回的日期范圍是1~31,又不能加1。
打印本地時區表示的日期和時間時,不同的計算機可能會有不同的結果。如果我們想要針對用戶的偏好精確地控制日期和時間的格式,就可以使用SimpleDateFormat對一個Date進行轉換。它用預定義的字符串表示格式化:
- yyyy:年
- MM:月
- dd: 日
- HH: 小時
- mm: 分鍾
- ss: 秒
我們來看如何以自定義的格式輸出:
Java的格式化預定義了許多不同的格式,我們以MMM和E為例:
上述代碼在不同的語言環境會打印出類似Sun Sep 15, 2019這樣的日期。可以從JDK文檔查看詳細的格式說明。一般來說,字母越長,輸出越長。以M為例,假設當前月份是9月:
- M:輸出9
- MM:輸出09
- MMM:輸出Sep
- MMMM:輸出September
Date對象有幾個嚴重的問題:
- 它不能轉換時區,除了toGMTString()可以按GMT+0:00輸出外,Date總是以當前計算機系統的默認時區為基礎進行輸出。
- 此外,我們也很難對日期和時間進行加減,計算兩個日期相差多少天,計算某個月第一個星期一的日期等。
Calendar:
Calendar可以用於獲取並設置年、月、日、時、分、秒,它和Date比,主要多了一個可以做簡單的日期和時間運算的功能。
我們來看Calendar的基本用法:
注意到Calendar獲取年月日這些信息變成了get(int field),返回的年份不必轉換,返回的月份仍然要加1,返回的星期要特別注意,1~7分別表示周日,周一,……,周六。
Calendar只有一種方式獲取,即Calendar.getInstance(),而且一獲取到就是當前時間。如果我們想給它設置成特定的一個日期和時間,就必須先清除所有字段:
利用Calendar.getTime()可以將一個Calendar對象轉換成Date對象,然后就可以用SimpleDateFormat進行格式化了。
TimeZone:
Calendar和Date相比,它提供了時區轉換的功能。時區用TimeZone對象表示:
時區的唯一標識是以字符串表示的ID,我們獲取指定TimeZone對象也是以這個ID為參數獲取,GMT+09:00、Asia/Shanghai都是有效的時區ID。要列出系統支持的所有ID,請使用TimeZone.getAvailableIDs()。
有了時區,我們就可以對指定時間進行轉換。例如,下面的例子演示了如何將北京時間2019-11-20 8:15:00
轉換為紐約時間:
可見,利用Calendar進行時區轉換的步驟是:
- 清除所有字段;
- 設定指定時區;
- 設定日期和時間;
- 創建SimpleDateFormat並設定目標時區;
- 格式化獲取的Date對象(注意Date對象無時區信息,時區信息存儲在SimpleDateFormat中)。
因此,本質上時區轉換只能通過SimpleDateFormat在顯示的時候完成。
Calendar也可以對日期和時間進行簡單的加減:
小結:
- 計算機表示的時間是以整數表示的時間戳存儲的,即Epoch Time,Java使用long型來表示以毫秒為單位的時間戳,通過System.currentTimeMillis()獲取當前時間戳。
- Java有兩套日期和時間的API:
- 舊的Date、Calendar和TimeZone;
- 新的LocalDateTime、ZonedDateTime、ZoneId等。
- 分別位於java.util和java.time包中。
LocalDateTime
從Java 8開始,java.time包提供了新的日期和時間API,主要涉及的類型有:
- 本地日期和時間:LocalDateTime,LocalDate,LocalTime;
- 帶時區的日期和時間:ZonedDateTime;
- 時刻:Instant(某一時刻);
-
- 時區:ZoneId,ZoneOffset;
- 時間間隔:Duration。
- 以及一套新的用於取代SimpleDateFormat的格式化類型DateTimeFormatter。
和舊的API相比,新API嚴格區分了時刻、本地日期、本地時間和帶時區的日期時間,並且,對日期和時間進行運算更加方便。
此外,新API修正了舊API不合理的常量設計:
- Month的范圍用1~12表示1月到12月;
- Week的范圍用1~7表示周一到周日。
- 最后,新API的類型幾乎全部是不變類型(和String類似),可以放心使用不必擔心被修改。
LocalDateTime:
我們首先來看最常用的LocalDateTime,它表示一個本地日期和時間:
本地日期和時間通過now()獲取到的總是以當前默認時區返回的,和舊API不同,LocalDateTime、LocalDate和LocalTime默認嚴格按照ISO 8601規定的日期和時間格式進行打印。
上述代碼其實有一個小問題,在獲取3個類型的時候,由於執行一行代碼總會消耗一點時間,因此,3個類型的日期和時間很可能對不上(時間的毫秒數基本上不同)。為了保證獲取到同一時刻的日期和時間,可以改寫如下:
LocalDateTime dt = LocalDateTime.now(); // 當前日期和時間
LocalDate d = dt.toLocalDate(); // 轉換到當前日期
LocalTime t = dt.toLocalTime(); // 轉換到當前時間
反過來,通過指定的日期和時間創建LocalDateTime可以通過of()方法:
// 指定日期和時間:
LocalDate d2 = LocalDate.of(2019, 11, 30); // 2019-11-30, 注意11=11月
LocalTime t2 = LocalTime.of(15, 16, 17); // 15:16:17
LocalDateTime dt2 = LocalDateTime.of(2019, 11, 30, 15, 16, 17);
LocalDateTime dt3 = LocalDateTime.of(d2, t2);
因為嚴格按照ISO 8601的格式,因此,將字符串轉換為LocalDateTime就可以傳入標准格式:
LocalDateTime dt = LocalDateTime.parse("2019-11-19T15:16:17");
LocalDate d = LocalDate.parse("2019-11-19");
LocalTime t = LocalTime.parse("15:16:17");
注意ISO 8601規定的日期和時間分隔符是T。標准格式如下:
- 日期:yyyy-MM-dd
- 時間:HH:mm:ss
- 帶毫秒的時間:HH:mm:ss.SSS
- 日期和時間:yyyy-MM-dd'T'HH:mm:ss
- 帶毫秒的日期和時間:yyyy-MM-dd'T'HH:mm:ss.SSS
DateTimeFormatter:
如果要自定義輸出的格式,或者要把一個非ISO 8601格式的字符串解析成LocalDateTime,可以使用新的DateTimeFormatter:
LocalDateTime提供了對日期和時間進行加減的非常簡單的鏈式調用:
注意到月份加減會自動調整日期,例如從2019-10-31減去1個月得到的結果是2019-09-30,因為9月沒有31日。
對日期和時間進行調整則使用withXxx()方法,例如:withHour(15)會把10:11:12變為15:11:12:
- 調整年:withYear()
- 調整月:withMonth()
- 調整日:withDayOfMonth()
- 調整時:withHour()
- 調整分:withMinute()
- 調整秒:withSecond()
示例代碼如下:
同樣注意到調整月份時,會相應地調整日期,即把2019-10-31的月份調整為9時,日期也自動變為30。實際上,LocalDateTime還有一個通用的with()方法允許我們做更復雜的運算。例如:
對於計算某個月第1個周日這樣的問題,新的API可以輕松完成。要判斷兩個LocalDateTime的先后,可以使用isBefore()、isAfter()方法,對於LocalDate和LocalTime類似:
注意到LocalDateTime無法與時間戳進行轉換,因為LocalDateTime沒有時區,無法確定某一時刻。后面我們要介紹的ZonedDateTime相當於LocalDateTime加時區的組合,它具有時區,可以與long表示的時間戳進行轉換。
Duration和Period:
Duration表示兩個時刻之間的時間間隔。另一個類似的Period表示兩個日期之間的天數:
注意到兩個LocalDateTime之間的差值使用Duration表示,類似PT1235H10M30S,表示1235小時10分鍾30秒。而兩個LocalDate之間的差值用Period表示,類似P1M21D,表示1個月21天。
Duration和Period的表示方法也符合ISO 8601的格式,它以P...T...的形式表示,P...T之間表示日期間隔,T后面表示時間間隔。如果是PT...的格式表示僅有時間間隔。利用ofXxx()或者parse()方法也可以直接創建Duration:
Duration d1 = Duration.ofHours(10); // 10 hours
Duration d2 = Duration.parse("P1DT2H3M"); // 1 day, 2 hours, 3 minutes
有的童鞋可能發現Java 8引入的java.timeAPI。怎么和一個開源的Joda Time很像?難道JDK也開始抄襲開源了?其實正是因為開源的Joda Time設計很好,應用廣泛,所以JDK團隊邀請Joda Time的作者Stephen Colebourne共同設計了java.timeAPI。
小結:
- Java 8引入了新的日期和時間API,它們是不變類,默認按ISO 8601標准格式化和解析;
- 使用LocalDateTime可以非常方便地對日期和時間進行加減,或者調整日期和時間,它總是返回新對象;
- 使用isBefore()和isAfter()可以判斷日期和時間的先后;
- 使用Duration和Period可以表示兩個日期和時間的“區間間隔”。
ZonedDateTime
LocalDateTime總是表示本地日期和時間,要表示一個帶時區的日期和時間,我們就需要ZonedDateTime。
可以簡單地把ZonedDateTime理解成LocalDateTime加ZoneId。ZoneId是java.time引入的新的時區類,注意和舊的java.util.TimeZone區別。
要創建一個ZonedDateTime對象,有以下幾種方法,
- 一種是通過now()方法返回當前時間:
觀察打印的兩個ZonedDateTime,發現它們時區不同,但表示的時間都是同一時刻(毫秒數不同是執行語句時的時間差) - 另一種方式是通過給一個LocalDateTime附加一個ZoneId,就可以變成ZonedDateTime:
以這種方式創建的ZonedDateTime,它的日期和時間與LocalDateTime相同,但附加的時區不同,因此是兩個不同的時刻
時區轉換:
要轉換時區,
- 首先我們需要有一個ZonedDateTime對象,
- 然后,通過withZoneSameInstant()將關聯時區轉換到另一個時區,轉換后日期和時間都會相應調整。
下面的代碼演示了如何將北京時間轉換為紐約時間:
要特別注意,時區轉換的時候,由於夏令時的存在,不同的日期轉換的結果很可能是不同的。
涉及到時區時,千萬不要自己計算時差,否則難以正確處理夏令時。
有了ZonedDateTime,將其轉換為本地時間就非常簡單:
轉換為LocalDateTime時,直接丟棄了時區信息。
ZonedDateTime仍然提供了plusDays()等加減操作。
小結:
- ZonedDateTime是帶時區的日期和時間,可用於時區轉換;
- ZonedDateTime和LocalDateTime可以相互轉換。
DateTimeFormatter
- 使用舊的Date對象時,我們用SimpleDateFormat進行格式化顯示。
- 使用新的LocalDateTime或ZonedLocalDateTime時,我們要進行格式化顯示,就要使用DateTimeFormatter。
和SimpleDateFormat不同的是,DateTimeFormatter不但是不變對象,它還是線程安全的。線程的概念我們會在后面涉及到。現在我們只需要記住:因為SimpleDateFormat不是線程安全的,使用的時候,只能在方法內部創建新的局部變量。而DateTimeFormatter可以只創建一個實例,到處引用。
創建DateTimeFormatter時,
- 我們仍然通過傳入格式化字符串實現:
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
格式化字符串的使用方式與SimpleDateFormat完全一致。 - 另一種創建DateTimeFormatter的方法是,傳入格式化字符串時,同時指定Locale:
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("E, yyyy-MMMM-dd HH:mm", Locale.US);
這種方式可以按照Locale默認習慣格式化。
我們來看實際效果:
在格式化字符串中,如果需要輸出固定字符,可以用'xxx'表示。
當我們直接調用System.out.println()對一個ZonedDateTime或者LocalDateTime實例進行打印的時候,實際上,調用的是它們的toString()方法,默認的toString()方法顯示的字符串就是按照ISO 8601格式顯示的,我們可以通過DateTimeFormatter預定義的幾個靜態變量來引用:
var ldt = LocalDateTime.now();
System.out.println(DateTimeFormatter.ISO_DATE.format(ldt));
System.out.println(DateTimeFormatter.ISO_DATE_TIME.format(ldt));
得到的輸出和toString()類似:
2019-09-15
2019-09-15T23:16:51.56217
小結:
- 對ZonedDateTime或LocalDateTime進行格式化,需要使用DateTimeFormatter類;
- DateTimeFormatter可以通過格式化字符串和Locale對日期和時間進行定制輸出。
Instant
我們已經講過,計算機存儲的當前時間,本質上只是一個不斷遞增的整數。Java提供的System.currentTimeMillis()返回的就是以毫秒表示的當前時間戳。
這個當前時間戳在java.time中以Instant類型表示,我們用Instant.now()獲取當前時間戳,效果和System.currentTimeMillis()類似:
實際上,Instant內部只有兩個核心字段:
public final class Instant implements ... {
private final long seconds;
private final int nanos;
}
一個是以秒為單位的時間戳,一個是更精確的納秒精度。它和System.currentTimeMillis()返回的long相比,只是多了更高精度的納秒。
既然Instant就是時間戳,那么,給它附加上一個時區,就可以創建出ZonedDateTime:
// 以指定時間戳創建Instant:
Instant ins = Instant.ofEpochSecond(1568568760);
ZonedDateTime zdt = ins.atZone(ZoneId.systemDefault());
System.out.println(zdt); // 2019-09-16T01:32:40+08:00[Asia/Shanghai]
可見,對於某一個時間戳,給它關聯上指定的ZoneId,就得到了ZonedDateTime,繼而可以獲得了對應時區的LocalDateTime。
所以,LocalDateTime,ZoneId,Instant,ZonedDateTime和long都可以互相轉換:
轉換的時候,只需要留意long類型以毫秒還是秒為單位即可。
小結
- Instant表示高精度時間戳,它可以和ZonedDateTime以及long互相轉換。
最佳實踐
由於Java提供了新舊兩套日期和時間的API,除非涉及到遺留代碼,否則我們應該堅持使用新的API。
如果需要與遺留代碼打交道,如何在新舊API之間互相轉換呢?
- 舊API轉新API:
如果要把舊式的Date或Calendar轉換為新API對象,可以通過toInstant()方法轉換為Instant對象,再繼續轉換為ZonedDateTime:
// Date -> Instant:
Instant ins1 = new Date().toInstant();
// Calendar -> Instant -> ZonedDateTime:
Calendar calendar = Calendar.getInstance();
Instant ins2 = calendar.toInstant();
ZonedDateTime zdt = ins2.atZone(calendar.getTimeZone().toZoneId());
從上面的代碼還可以看到,舊的TimeZone提供了一個toZoneId(),可以把自己變成新的ZoneId。
- 新API轉舊API:
如果要把新的ZonedDateTime轉換為舊的API對象,只能借助long型時間戳做一個“中轉”:
// ZonedDateTime -> long:
ZonedDateTime zdt = ZonedDateTime.now();
long ts = zdt.toEpochSecond() * 1000;
// long -> Date:
Date date = new Date(ts);
// long -> Calendar:
Calendar calendar = Calendar.getInstance();
calendar.clear();
calendar.setTimeZone(TimeZone.getTimeZone(zdt.getZone().getId()));
calendar.setTimeInMillis(zdt.toEpochSecond() * 1000);
從上面的代碼還可以看到,新的ZoneId轉換為舊的TimeZone,需要借助ZoneId.getId()返回的String完成。
在數據庫中存儲日期和時間:
除了舊式的java.util.Date,我們還可以找到另一個java.sql.Date,它繼承自java.util.Date,但會自動忽略所有時間相關信息。這個奇葩的設計原因要追溯到數據庫的日期與時間類型。
在數據庫中,也存在幾種日期和時間類型:
- DATETIME:表示日期和時間;
- DATE:僅表示日期;
- TIME:僅表示時間;
- TIMESTAMP:和DATETIME類似,但是數據庫會在創建或者更新記錄的時候同時修改TIMESTAMP。
在使用Java程序操作數據庫時,我們需要把數據庫類型與Java類型映射起來。下表是數據庫類型與Java新舊API的映射關系:
實際上,在數據庫中,我們需要存儲的最常用的是時刻(Instant),因為有了時刻信息,就可以根據用戶自己選擇的時區,顯示出正確的本地時間。所以,最好的方法是直接用長整數long表示,在數據庫中存儲為BIGINT類型。
通過存儲一個long型時間戳,我們可以編寫一個timestampToString()的方法,非常簡單地為不同用戶以不同的偏好來顯示不同的本地時間:
小結:
- 處理日期和時間時,盡量使用新的java.time包;
- 在數據庫中存儲時間戳時,盡量使用long型時間戳,它具有省空間,效率高,不依賴數據庫的優點。