上半年春招的時候,作為面試官,對于面試表現的不錯的同學會要求其寫一小段代碼看看。題目很簡單:
給定一個日期,然后計算下距離今天相差的天數。
本以為這么個問題就是用來活躍面試氛圍的,但是結果卻讓人大跌眼鏡,真正能寫出來的人竟然寥寥無幾,很多人寫了一整張A4紙都寫不下,最后還是沒寫完...他們在做什么?先取出今天的日期,然后分別計算得出年、月、日的值,然后將給定的字符串進行切割,得到目標的年、月、日,然后再判斷是否閏年之類的邏輯,決定每月應該是加28天還是29天還是30或者31天,最后得出一個天數!
想想都令人窒息的操作...
日期時間的處理,是軟件開發中極其常見的場景,JAVA中與日期、時間相關的一些類與API方法也很多,這里結合平時的編碼實踐全面的整理了下,希望可以幫助大家厘清其中的門道,更加游刃有余的面對此方面的處理~
JAVA中與日期時間相關的類
java.util包中
類名 |
具體描述 |
Date |
Date對象算是JAVA中歷史比較悠久的用于處理日期、時間相關的類了,但是隨著版本的迭代演進,其中的眾多方法都已經被棄用,所以Date更多的時候僅被用來做一個數據類型使用,用于記錄對應的日期與時間信息 |
Calender |
為了彌補Date對象在日期時間處理方法上的一些缺陷,JAVA提供了Calender抽象類來輔助實現Date相關的一些日歷日期時間的處理與計算。 |
TimeZone |
Timezone類提供了一些有用的方法用于獲取時區的相關信息 |
java.time包中
JAVA8之后新增了java.time包,提供了一些與日期時間有關的新實現類:
具體每個類對應的含義梳理如下表:
類名 |
含義說明 |
LocalDate |
獲取當前的日期信息,僅有簡單的日期信息,不包含具體時間、不包含時區信息。 |
LocalTime |
獲取當前的時間信息,僅有簡單的時間信息,不含具體的日期、時區信息。 |
LocalDateTime |
可以看做是LocalDate和LocalTime的組合體,其同時含有日期信息與時間信息,但是依舊不包含任何時區信息。 |
OffsetDateTime |
在LocalDateTime基礎上增加了時區偏移量信息 |
ZonedDateTime |
在OffsetDateTime基礎上,增加了時區信息 |
ZoneOffset |
時區偏移量信息, 比如+8:00或者-5:00等 |
ZoneId |
具體的時區信息,比如Asia/Shanghai或者America/Chicago |
時間間隔計算
Period與Duration類
JAVA8開始新增的java.time包中有提供Duration和Period兩個類,用于處理日期時間間隔相關的場景,兩個類的區別點如下:
類 |
描述 |
Duration |
時間間隔,用于秒級的時間間隔計算 |
Period |
日期間隔,用于天級別的時間間隔計算,比如年月日維度的 |
Duration與Period具體使用的時候還需要有一定的甄別,因為部分的方法很容易使用中被混淆,下面分別說明下。
- Duration
Duration的最小計數單位為納秒,其內部使用seconds和nanos兩個字段來進行組合計數表示duration總長度。
Duration的常用API方法梳理如下:
方法 |
描述 |
between |
計算兩個時間的間隔,默認是秒 |
ofXxx |
以of開頭的一系列方法,表示基于給定的值創建一個Duration實例。比如ofHours(2L),則表示創建一個Duration對象,其值為間隔2小時 |
plusXxx |
以plus開頭的一系列方法,用于在現有的Duration值基礎上增加對應的時間長度,比如plusDays()表示追加多少天,或者plusMinutes()表示追加多少分鐘 |
minusXxx |
以minus開頭的一系列方法,用于在現有的Duration值基礎上扣減對應的時間長度,與plusXxx相反 |
toXxxx |
以to開頭的一系列方法,用于將當前Duration對象轉換為對應單位的long型數據,比如toDays()表示將當前的時間間隔的值,轉換為相差多少天,而toHours()則標識轉換為相差多少小時。 |
getSeconds |
獲取當前Duration對象對應的秒數, 與toXxx方法類似,只是因為Duration使用秒作為計數單位,所以直接通過get方法即可獲取到值,而toDays()是需要通過將秒數轉為天數換算之后返回結果,所以提供的方法命名上會有些許差異。 |
getNano |
獲取當前Duration對應的納秒數“零頭”。注意這里與toNanos()不一樣,toNanos是Duration值的納秒單位總長度,getNano()只是獲取不滿1s剩余的那個零頭,以納秒表示。 |
isNegative |
檢查Duration實例是否小于0,若小于0返回true, 若大于等于0返回false |
isZero |
用于判斷當前的時間間隔值是否為0 ,比如比較兩個時間是否一致,可以通過between計算出Duration值,然后通過isZero判斷是否沒有差值。 |
withSeconds |
對現有的Duration對象的nanos零頭值不變的情況下,變更seconds部分的值,然后返回一個新的Duration對象 |
withNanos |
對現有的Duration對象的seconds值不變的情況下,變更nanos部分的值,然后返回一個新的Duration對象 |
關于Duration的主要API的使用,參見如下示意:
public void testDuration() {
LocalTime target = LocalTime.parse("00:02:35.700");
// 獲取當前日期,此處為了保證后續結果固定,注掉自動獲取當前日期,指定固定日期
// LocalDate today = LocalDate.now();
LocalTime today = LocalTime.parse("12:12:25.600");
// 輸出:12:12:25.600
System.out.println(today);
// 輸出:00:02:35.700
System.out.println(target);
Duration duration = Duration.between(target, today);
// 輸出:PT12H9M49.9S
System.out.println(duration);
// 輸出:43789
System.out.println(duration.getSeconds());
// 輸出:900000000
System.out.println(duration.getNano());
// 輸出:729
System.out.println(duration.toMinutes());
// 輸出:PT42H9M49.9S
System.out.println(duration.plusHours(30L));
// 輸出:PT15.9S
System.out.println(duration.withSeconds(15L));
}
- Period
Period相關接口與Duration類似,其計數的最小單位是天,看下Period內部時間段記錄采用了年、月、日三個field來記錄:
常用的API方法列舉如下:
方法 |
描述 |
between |
計算兩個日期之間的時間間隔。注意,這里只能計算出相差幾年幾個月幾天。 |
ofXxx |
of()或者以of開頭的一系列static方法,用于基于傳入的參數構造出一個新的Period對象 |
withXxx |
以with開頭的方法,比如withYears、withMonths、withDays等方法,用于對現有的Period對象中對應的年、月、日等字段值進行修改(只修改對應的字段,比如withYears方法,只修改year,保留month和day不變),并生成一個新的Period對象 |
getXxx |
讀取Period中對應的year、month、day字段的值。注意下,這里是僅get其中的一個字段值,而非整改Period的不同單位維度的總值。 |
plusXxx |
對指定的字段進行追加數值操作 |
minusXxx |
對指定的字段進行扣減數值操作 |
isNegative |
檢查Period實例是否小于0,若小于0返回true, 若大于等于0返回false |
isZero |
用于判斷當前的時間間隔值是否為0 ,比如比較兩個時間是否一致,可以通過between計算出Period值,然后通過isZero判斷是否沒有差值。 |
關于Period的主要API的使用,參見如下示意:
public void calculateDurationDays() {
LocalDate target = LocalDate.parse("2021-07-11");
// 獲取當前日期,此處為了保證后續結果固定,注掉自動獲取當前日期,指定固定日期
// LocalDate today = LocalDate.now();
LocalDate today = LocalDate.parse("2022-07-08");
// 輸出:2022-07-08
System.out.println(today);
// 輸出:2021-07-11
System.out.println(target);
Period period = Period.between(target, today);
// 輸出:P11M27D, 表示11個月27天
System.out.println(period);
// 輸出:0, 因為period值為11月27天,即year字段為0
System.out.println(period.getYears());
// 輸出:11, 因為period值為11月27天,即month字段為11
System.out.println(period.getMonths());
// 輸出:27, 因為period值為11月27天,即days字段為27
System.out.println(period.getDays());
// 輸出:P14M27D, 因為period為11月27天,加上3月,變成14月27天
System.out.println(period.plusMonths(3L));
// 輸出:P11M15D,因為period為11月27天,僅將days值設置為15,則變為11月15天
System.out.println(period.withDays(15));
// 輸出:P2Y3M44D
System.out.println(Period.of(2, 3, 44));
}
Duration與Period踩坑記
Duration與Period都是用于日期之間的計算操作。Duration主要用于秒、納秒等維度的數據處理與計算。Period主要用于計算年、月、日等維度的數據處理與計算。
先看個例子,計算兩個日期相差的天數,使用Duration的時候:
public void calculateDurationDays(String targetDate) {
LocalDate target = LocalDate.parse(targetDate);
LocalDate today = LocalDate.now();
System.out.println("today : " + today);
System.out.println("target: " + target);
long days = Duration.between(target, today).abs().toDays();
System.out.println("相差:" + days + "天");
}
運行后會報錯:
today : 2022-07-07
target: 2022-07-11
Exception in thread "main" java.time.temporal.UnsupportedTemporalTypeException: Unsupported unit: Seconds
at java.time.LocalDate.until(LocalDate.java:1614)
at java.time.Duration.between(Duration.java:475)
at com.veezean.demo5.DateService.calculateDurationDays(DateService.java:24)
點擊看下Duration.between源碼,可以看到注釋上明確有標注著,這個方法是用于秒級的時間段間隔計算,而我們這里傳入的是兩個天級別的數據,所以就不支持此類型運算,然后拋異常了。
再看下使用Period的實現:
public void calculateDurationDays(String targetDate) {
LocalDate target = LocalDate.parse(targetDate);
LocalDate today = LocalDate.now();
System.out.println("today : " + today);
System.out.println("target: " + target);
// 注意,此處寫法錯誤!這里容易踩坑:
long days = Math.abs(Period.between(target, today).getDays());
System.out.println("相差:" + days + "天");
}
執行結果:
today : 2022-07-07
target: 2021-07-07
相差:0天
執行是不報錯,但是結果明顯是錯誤的。這是因為getDays()并不會將Period值換算為天數,而是單獨計算年、月、日,此處只是返回天數這個單獨的值。
再看下面的寫法:
public void calculateDurationDays(String targetDate) {
LocalDate target = LocalDate.parse(targetDate);
LocalDate today = LocalDate.now();
System.out.println("today : " + today);
System.out.println("target: " + target);
Period between = Period.between(target, today);
System.out.println("相差:"
+ Math.abs(between.getYears()) + "年"
+ Math.abs(between.getMonths()) + "月"
+ Math.abs(between.getDays()) + "天");
}
結果為:
today : 2022-07-07
target: 2021-07-11
相差:0年11月26天
所以說,如果想要計算兩個日期之間相差的絕對天數,用Period不是一個好的思路。
計算日期差
- 通過LocalDate來計算
LocalDate中的toEpocDay可返回當前時間距離原點時間之間的天數,可以基于這一點,來實現計算兩個日期之間相差的天數:
代碼如下:
public void calculateDurationDays(String targetDate) {
LocalDate target = LocalDate.parse(targetDate);
LocalDate today = LocalDate.now();
System.out.println("today : " + today);
System.out.println("target: " + target);
long days = Math.abs(target.toEpochDay() - today.toEpochDay());
System.out.println("相差:" + days + "天");
}
結果為:
today : 2022-07-07
target: 2021-07-11
相差:361天
- 通過時間戳來計算
如果是使用的Date對象,則可以通過將Date日期轉換為毫秒時間戳的方式相減然后將毫秒數轉為天數的方式來得到結果。需要注意的是通過毫秒數計算日期天數的差值時,需要屏蔽掉時分秒帶來的誤差影響。
public void calculateDaysGap(Date start, Date end) {
final long ONE_DAY_MILLIS = 1000L * 60 * 60 * 24;
// 此處要注意,去掉時分秒的差值影響,此處采用先換算為天再相減的方式
long gapDays = Math.abs(end.getTime()/ONE_DAY_MILLIS - start.getTime()/ONE_DAY_MILLIS);
System.out.println(gapDays);
}
輸出結果:
today : 2022-07-08
target: 2021-07-11
相差:362天
- 數學邏輯計算
分別算出年、月、日差值,然后根據是否閏年、每月是30還是31天等計數邏輯,純數學硬懟方式計算。
不推薦、代碼略...
計算接口處理耗時
在一些性能優化的場景中,我們需要獲取到方法處理的執行耗時,很多人都是這么寫的:
public void doSomething() {
// 記錄開始時間戳
long startMillis = System.currentTimeMillis();
// do something ...
// 計算結束時間戳
long endMillis = System.currentTimeMillis();
// 計算相差的毫秒數
System.out.println(endMillis - startMillis);
}
當然啦,如果你使用的是JDK8+的版本,你還可以這么寫:
public void doSomething() {
// 記錄開始時間戳
Instant start = Instant.now();
// do something ...
// 計算結束時間戳
Instant end = Instant.now();
// 計算相差的毫秒數
System.out.println(Duration.between(start, end).toMillis());
}
時間格式轉換
項目中,時間格式轉換是一個非常典型的日期處理操作,可能會涉及到將一個字符串日期轉換為JAVA對象,或者是將一個JAVA日期對象轉換為指定格式的字符串日期時間。
SimpleDataFormat實現
在JAVA8之前,通常會使用SimpleDateFormat類來處理日期與字符串之間的相互轉換:
public void testDateFormatter() {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 日期轉字符串
String format = simpleDateFormat.format(new Date());
System.out.println("當前時間:" + format);
try {
// 字符串轉日期
Date parseDate = simpleDateFormat.parse("2022-07-08 06:19:27");
System.out.println("轉換后Date對象: " + parseDate);
// 按照指定的時區進行轉換,可以對比下前面轉換后的結果,會發現不一樣
simpleDateFormat.setTimeZone(TimeZone.getTimeZone("GMT+5:00"));
parseDate = simpleDateFormat.parse("2022-07-08 06:19:27");
System.out.println("指定時區轉換后Date對象: " + parseDate);
} catch (Exception e) {
e.printStackTrace();
}
}
輸出結果如下:
當前時間:2022-07-08 06:25:31
轉換后Date對象: Fri Jul 08 06:19:27 CST 2022
指定時區轉換后Date對象: Fri Jul 08 09:19:27 CST 2022
補充說明:
SimpleDateFormat對象是非線程安全的,所以項目中在封裝為工具方法使用的時候需要特別留意,最好結合ThreadLocal來適應在多線程場景的正確使用。
JAVA8之后,推薦使用DateTimeFormat替代SimpleDateFormat。
DataTimeFormatter實現
JAVA8開始提供的新的用于日期與字符串之間轉換的類,它很好的解決了SimpleDateFormat多線程的弊端,也可以更方便的與java.time中心的日期時間相關類的集成調用。
public void testDateFormatter() {
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime localDateTime = LocalDateTime.now();
// 格式化為字符串
String format = localDateTime.format(dateTimeFormatter);
System.out.println("當前時間:" + format);
// 字符串轉Date
LocalDateTime parse = LocalDateTime.parse("2022-07-08 06:19:27", dateTimeFormatter);
Date date = Date.from(parse.atZone(ZoneId.systemDefault()).toInstant());
System.out.println("轉換后Date對象: " + date);
}
輸出結果:
當前時間:2022-07-08 18:37:46
轉換后Date對象: Fri Jul 08 06:19:27 CST 2022
日期時間格式模板
對于計算機而言,時間處理的時候按照基于時間原點的數字進行處理即可,但是轉為人類方便識別的場景顯示時,經常會需要轉換為不同的日期時間顯示格式,比如:
2022-07-08 12:02:34
2022/07/08 12:02:34.238
2022年07月08日 12點03分48秒
在JAVA中,為了方便各種格式轉換,提供了基于時間模板進行轉換的實現能力:
時間格式模板中的字幕含義說明如下:
字母 |
使用說明 |
yyyy |
4位數的年份 |
yy |
顯示2位數的年份,比如2022年,則顯示為22年 |
MM |
顯示2位數的月份,不滿2位數的,前面補0,比如7月份顯示07月 |
M |
月份,不滿2位的月份不會補0 |
dd |
天, 如果1位數的天數,則補0 |
d |
天,不滿2位數字的,不補0 |
HH |
24小時制的時間顯示,小時數,兩位數,不滿2位數字的前面補0 |
H |
24小時制的時間顯示,小時數,不滿2位數字的不補0 |
hh |
12小時制的時間顯示,小時數,兩位數,不滿2位數字的前面補0 |
ss |
秒數,不滿2位的前面補0 |
s |
秒數,不滿2位的不補0 |
SSS |
毫秒數 |
z |
時區名稱,比如北京時間東八區,則顯示CST |
Z |
時區偏移信息,比如北京時間東八區,則顯示+0800 |
消失的8小時問題
日期字符串存入DB后差8小時
在后端與數據庫交互的時候,可能會遇到一個問題,就是往DB中存儲了一個時間字段之后,后面再查詢的時候,就會發現時間數值差了8個小時,這個需要在DB的連接信息中指定下時區信息:
spring.datasource.druid.url=jdbc:MySQL://127.0.0.1:3306/test?serverTimezone=Asia/Shanghai
界面時間與后臺時間差8小時
在有一些前后端交互的項目中,可能會遇到一個問題,就是前端選擇并保存了一個時間信息,再查詢的時候就會發現與設置的時間差了8個小時,這個其實就是后端時區轉換設置的問題。
SpringBoot的配置文件中,需要指定時間字符串轉換的時區信息:
spring.jackson.time-zone=GMT+8
這樣從接口json中傳遞過來的時間信息,jackson框架可以根據對應時區轉換為正確的Date數據進行處理。
我是悟道,聊技術、又不僅僅聊技術~
如果覺得有用,請點個關注,也可以關注下我的公眾號【架構悟道】,獲取更及時的更新。
期待與你一起探討,一起成長為更好的自己。