七、日期和時間
文章目錄
Java標準庫有兩套處理日期和時間的API:
- 一套定義在
java.util
這個包裏面,主要包括Date
、Calendar
和TimeZone
這幾個類; - 一套新的API是在Java 8引入的,定義在
java.time
這個包裏面,主要包括LocalDateTime
、ZonedDateTime
、ZoneId
等。
在舊的API中存在有很多問題,所以引入了新的API。但很多時候,我們仍然需要在兩種對象之間進行轉換。
在使用日期和時間時,除非涉及到遺留代碼,否則應該堅持使用新的API。
1、Date 和 Calendar
1.1 Date格式輸出
@Test
public void m0() {
Date date = new Date(); //獲取當前時間
DateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); //自定義格式輸出
System.out.println(sdf.format(date));
}
Date中的
getDate()
、getDay()
、getHours()
、getMinites()
等方法已棄用。
1.2 Calendar
Calendar
可以用於獲取並設置年、月、日、時、分、秒,它和Date
比,主要多了一個可以做簡單的日期和時間運算的功能。基本用法如下:
@Test
public void m0() {
// 獲取當前時間:
Calendar c = Calendar.getInstance();
int y = c.get(Calendar.YEAR);
int m = 1 + c.get(Calendar.MONTH);
int d = c.get(Calendar.DAY_OF_MONTH);
int w = c.get(Calendar.DAY_OF_WEEK);
int hh = c.get(Calendar.HOUR_OF_DAY);
int mm = c.get(Calendar.MINUTE);
int ss = c.get(Calendar.SECOND);
int ms = c.get(Calendar.MILLISECOND);
System.out.println(y + "-" + m + "-" + d + " " + w + " " + hh + ":" + mm + ":" + ss + "." + ms);
}
//輸出結果
2020-3-10 3 21:10:20.43
Calendar
獲取年月日這些信息變成了get(int field)
,返回的年份不必轉換,返回的月份必須要加1。返回的星期:,
1
~7
分別表示週日,週一,……,週六。
Calendar
只有一種獲取方式,即Calendar.getInstance()
獲取當地時間。
給Calendar
設置一個特定的日期和時間:
@Test
public void m1() {
// 當前時間:
Calendar c = Calendar.getInstance();
// 清除所有:
c.clear();
// 設置2019年:
c.set(Calendar.YEAR, 2019);
// 設置9月:注意8表示9月:
c.set(Calendar.MONTH, 8);
// 設置2日:
c.set(Calendar.DATE, 2);
// 設置時間:
c.set(Calendar.HOUR_OF_DAY, 21);
c.set(Calendar.MINUTE, 22);
c.set(Calendar.SECOND, 23);
//getTime() 可以將Calendar對象轉換爲Date對象
String format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(c.getTime());
System.out.println(format); //2019-09-02 21:22:23
}
2、LocalDateTime
從Java 8開始,java.time
包提供了新的日期和時間API,主要涉及的類型有:
- 本地日期和時間:
LocalDateTime
,LocalDate
,LocalTime
; - 帶時區的日期和時間:
ZonedDateTime
; - 時刻:
Instant
; - 時區:
ZoneId
,ZoneOffset
; - 時間間隔:
Duration
。
以及一套新的用於取代SimpleDateFormat
的格式化類型DateTimeFormatter
。
新的API:
- 嚴格區分了時刻、本地日期、本地時間和帶失去的日期時間,並且對日期和時間的運算更加方便。
- Month用1~12表示1月到12月;Week用1~7表示週一到週日;
- 類型幾乎都是不變類型(類似String),不必擔心被修改。
2.1 LocalDateTime
@Test
public void m1() {
LocalDate d = LocalDate.now(); // 當前日期
LocalTime t = LocalTime.now(); // 當前時間
LocalDateTime dt = LocalDateTime.now(); // 當前日期和時間
System.out.println(d); // 嚴格按照ISO 8601格式打印
System.out.println(t); // 嚴格按照ISO 8601格式打印
System.out.println(dt); // 嚴格按照ISO 8601格式打印
}
//輸出結果
2020-03-10
21:48:13.924
2020-03-10T21:48:13.924
2.2 日期和時間 <-> 日期、時間
LocalDateTime dt = LocalDateTime.now(); // 當前日期和時間
LocalDate d = dt.toLocalDate(); // 轉換到當前日期
LocalTime t = dt.toLocalTime(); // 轉換到當前時間
// 指定日期和時間:
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);
2.3 String ->LocalDateTime
因爲嚴格按照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
2.4 DateTimeFormater
如果要自定義輸出的格式,或者要把一個非ISO 8601格式的字符串解析成LocalDateTime
,可以使用新的DateTimeFormatter
:
@Test
public void m1() {
// 自定義格式化:
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
System.out.println(dtf.format(LocalDateTime.now()));
// 用自定義格式解析:
LocalDateTime dt2 = LocalDateTime.parse("2020/03/09 15:16:17", dtf);
System.out.println(dt2);
}
//輸出結果
2020/03/10 22:02:57
2020-03-09T15:16:17
2.5 LocalDateTime 的計算
LocalDateTime
提供了對日期和時間的加減計算方法:
@Test
public void m1() {
LocalDateTime dt = LocalDateTime.of(2020, 03, 10, 20, 30, 59);
System.out.println(dt); //2020-03-10T20:30:59
// 加5天減3小時:
LocalDateTime dt2 = dt.plusDays(5).minusHours(3);
System.out.println(dt2); // 2020-03-15T17:30:59
// 減1月:
LocalDateTime dt3 = dt2.minusMonths(1);
System.out.println(dt3); // 2020-02-15T17:30:59
// 日期變爲15日:
LocalDateTime dt4 = dt.withDayOfMonth(15);
System.out.println(dt4); // 2020-03-15T20:30:59
// 月份變爲9:
LocalDateTime dt5 = dt2.withMonth(9);
System.out.println(dt5); // 2020-09-15T17:30:59
}
對日期和時間進行調整則使用
withXxx()
方法,例如:withHour(15)
會把10:11:12
變爲15:11:12
:
- 調整年:withYear()
- 調整月:withMonth()
- 調整日:withDayOfMonth()
- 調整時:withHour()
- 調整分:withMinute()
- 調整秒:withSecond()
調整月時,如果填入錯誤日期,例如32,會報錯。其餘類似。
調整月份時,會相應地調整日期,即把
2020-10-31
的月份調整爲9
時,日期也自動變爲30
。
2.6 with()
方法
LocalDateTime
還有一個通用的with()
方法允許我們做更復雜的運算。例如:
@Test
public void m1() {
// 本月第一天0:00時刻:
LocalDateTime firstDay = LocalDate.now().withDayOfMonth(1).atStartOfDay();
System.out.println(firstDay); //2020-03-01T00:00
// 本月最後1天:
LocalDate lastDay = LocalDate.now().with(TemporalAdjusters.lastDayOfMonth());
System.out.println(lastDay); //2020-03-31
// 下月第1天:
LocalDate nextMonthFirstDay = LocalDate.now().with(TemporalAdjusters.firstDayOfNextMonth());
System.out.println(nextMonthFirstDay); //2020-04-01
// 本月第1個週一:
LocalDate firstWeekday = LocalDate.now().with(TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY));
System.out.println(firstWeekday); //2020-03-02
}
2.7 日期時間先後判斷
要判斷兩個LocalDateTime
的先後,可以使用isBefore()
、isAfter()
方法,對於LocalDate
和LocalTime
類似:
@Test
public void m1() {
LocalDateTime now = LocalDateTime.now();
LocalDateTime target = LocalDateTime.of(2019, 11, 19, 8, 15, 0);
System.out.println(now.isBefore(target));
System.out.println(LocalDate.now().isBefore(LocalDate.of(2019, 11, 19)));
System.out.println(LocalTime.now().isAfter(LocalTime.parse("08:15:00")));
}
2.8 Duration和Period
Duration
表示兩個時刻之間的時間間隔。另一個類似的Period
表示兩個日期之間的天數:
@Test
public void m1() {
LocalDateTime start = LocalDateTime.of(2019, 11, 19, 8, 15, 0);
LocalDateTime end = LocalDateTime.of(2020, 1, 9, 19, 25, 30);
Duration d = Duration.between(start, end);
System.out.println(d); // PT1235H10M30S
Period p = LocalDate.of(2019, 11, 19).until(LocalDate.of(2020, 1, 9));
System.out.println(p); // P1M21D
}
兩個
LocalDateTime
之間的差值使用Duration
表示,類似PT1235H10M30S
,表示1235小時10分鐘30秒。兩個
LocalDate
之間的差值用Period
表示,類似P1M21D
,表示1個月21天。
3、ZonedDateTime
當要表示一個帶時區的日期和時間時,就需要用到 ZonedDateTime
。
可以簡單地把ZonedDateTime
理解成LocalDateTime
加ZoneId
。ZoneId
是java.time
引入的新的時區類。
要創建一個 ZonedDateTime
對象,有以下幾種方法:
/*方法一:通過 `now()` 方法返回當前時間*/
@Test
public void m2() {
ZonedDateTime zbj = ZonedDateTime.now(); // 默認時區
ZonedDateTime zny = ZonedDateTime.now(ZoneId.of("America/New_York")); //用指定時區獲取當前時間
System.out.println(zbj); //2020-03-10T22:42:47.322+08:00[Asia/Shanghai]
System.out.println(zny); //2020-03-10T10:42:47.323-04:00[America/New_York]
}
/*方法二:通過給 `LocalDateTime` 附加一個 `ZoneId`*/
@Test
public void m2() {
LocalDateTime ldt = LocalDateTime.of(2019, 9, 15, 15, 16, 17);
ZonedDateTime zbj = ldt.atZone(ZoneId.systemDefault());
ZonedDateTime zny = ldt.atZone(ZoneId.of("America/New_York"));
System.out.println(zbj); //2019-09-15T15:16:17+08:00[Asia/Shanghai]
System.out.println(zny); //2019-09-15T15:16:17-04:00[America/New_York]
}
方法一中:打印的兩個
ZonedDateTime
,雖然時區不同,但表示的是同一時刻。方法二中:以這種方式創建的
ZonedDateTime
,它的日期和時間與LocalDateTime
相同,但附加的時區不同,因此是兩個不同的時刻
3.1 時區轉換
用withZoneSameInstant()
可以將關聯時區轉化到另一時區,轉化後日期和時間都會相應調整。
/*將北京時間調整爲紐約時間*/
@Test
public void m1() {
// 以中國時區獲取當前時間:
ZonedDateTime zbj = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
// 轉換爲紐約時間:
ZonedDateTime zny = zbj.withZoneSameInstant(ZoneId.of("America/New_York"));
System.out.println(zbj); //2020-03-10T23:33:44.988+08:00[Asia/Shanghai]
System.out.println(zny); //2020-03-10T11:33:44.988-04:00[America/New_York]
}
涉及到時區時,千萬不要自己計算時差,否則難以正確處理夏令時。
有了ZonedDateTime
,將其轉化爲本地時間就非常簡單:
ZonedDateTime zdt = ...
LocalDateTime ldt = zdt.toLocalDateTime();
4、Instant
計算機存儲當前的時間,本質上只是一個不斷增長的整數。
Java提供的System.currentTimeMillis()
返回的就是以毫秒錶示的當前時間戳。
在java.time
中當前的時間戳以Instant
類型表示,我們用Instant.now()
獲取當前時間戳:
@Test
public void m1() {
Instant now = Instant.now();
System.out.println(now.getEpochSecond()); // 秒,1583850200
System.out.println(now.toEpochMilli()); // 毫秒,1583850200151
}
在Instant
內部只有兩個核心字段:
public final class Instant implements ... {
private final long seconds;
private final int nanos;
}
一個是以秒爲單位的時間戳,另一個是更精確的納秒精度。
給 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
都可以互相轉換。
5、最佳實踐
在使用日期和時間時,除非涉及到遺留代碼,否則應該堅持使用新的API。
5.1 舊API ->新API
如果要把舊式的Date
或Calendar
轉換爲新API對象,可以通過toInstant()
方法轉換爲Instant
對象,再繼續轉換爲ZonedDateTime
:
// Date -> Instant -> ZonedDateTime:
Date date = new Date();
Instant ins1 = date.toInstant();
ZonedDateTime zdt1 = ins1.atZone(ZoneId.systemDefault());
// Calendar -> Instant -> ZonedDateTime:
Calendar calendar = Calendar.getInstance();
Instant ins2 = calendar.toInstant();
ZonedDateTime zdt2 = ins2.atZone(calendar.getTimeZone().toZoneId());
從上面的代碼還可以看到,舊的
TimeZone
提供了一個toZoneId()
,可以把自己變成新的ZoneId
。
5.2 新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
完成。
5.3 在數據庫中存儲日期和時間
除了舊式的java.util.Date
,我們還可以找到另一個java.sql.Date
,它繼承自java.util.Date
,但會自動忽略所有時間相關信息。
在數據庫中,也存在幾種日期和時間類型:
DATETIME
:表示日期和時間;DATE
:僅表示日期;TIME
:僅表示時間;TIMESTAMP
:和DATETIME
類似,但是數據庫會在創建或者更新記錄的時候同時修改TIMESTAMP
。
在操作數據庫時,我們需要把數據庫類型與Java類型映射起來。下表是數據庫類型與Java新舊API的映射關係:
數據庫 | 對應Java類(舊) | 對應Java類(新) |
---|---|---|
DATETIME | java.util.Date | LocalDateTime |
DATE | java.sql.Date | LocalDate |
TIME | java.sql.Time | LocalTime |
TIMESTAMP | java.sql.Timestamp | LocalDateTime |
實際上,在數據庫中,我們需要存儲的最常用的是時刻(Instant
),因爲有了時刻信息,就可以根據用戶自己選擇的時區,顯示出正確的本地時間。所以,最好的方法是直接用長整數long
表示,在數據庫中存儲爲BIGINT
類型。
下面是通過timestampToString
方法,將存儲的long
型時間戳轉化爲不用用戶的本地時間:
public class Main {
public static void main(String[] args) {
long ts = 1583856181452L;
System.out.println(timestampToString(ts, Locale.CHINA, "Asia/Shanghai"));
System.out.println(timestampToString(ts, Locale.US, "America/New_York"));
}
static String timestampToString(long epochMilli, Locale lo, String zoneId) {
Instant ins = Instant.ofEpochMilli(epochMilli);
DateTimeFormatter f = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT);
return f.withLocale(lo).format(ZonedDateTime.ofInstant(ins, ZoneId.of(zoneId)));
}
}