Java新舊時(shí)間日期API的使用和避坑指南
在 Java 8 之前,我們處理日期時(shí)間需求時(shí),使用 Date、Calender 和 SimpleDateFormat,來(lái)聲明時(shí)間戳、使用日歷處理日期和格式化解析日期時(shí)間。但是,這些類(lèi)的 API 的缺點(diǎn)比較明顯,比如可讀性差、易用性差、使用起來(lái)冗余繁瑣,還有線(xiàn)程安全問(wèn)題。因此,Java 8 推出了新的日期時(shí)間類(lèi)。
每一個(gè)類(lèi)功能明確清晰、類(lèi)之間協(xié)作簡(jiǎn)單、API 定義清晰不踩坑,API 功能強(qiáng)大無(wú)需借助外部工具類(lèi)即可完成操作,并且線(xiàn)程安全。
Java 8引入了三個(gè)新的日期時(shí)間類(lèi),分別是LocalDate、LocalTime和LocalDateTime,分別處理日期、時(shí)間和日期時(shí)間。
一、新的時(shí)間和日期API
1.1 獲取當(dāng)前時(shí)間
LocalDateTime localDateTime = LocalDateTime.now();
System.out.println("當(dāng)前時(shí)刻:" + localDateTime );
System.out.println("當(dāng)前年:" + localDateTime.getYear() +
"\n當(dāng)前月:" + localDateTime.getMonth() +
"\n當(dāng)前日:" + localDateTime.getDayOfMonth());
System.out.println("當(dāng)前時(shí)/分/秒:" +
localDateTime.getHour() +" / " +
localDateTime.getMinute() + "/" +
localDateTime.getSecond());
/*
* 打印結(jié)果
*
* 當(dāng)前時(shí)刻:2020-09-04T22:11:27.505361600
* 當(dāng)前年:2020
* 當(dāng)前月:SEPTEMBER
* 當(dāng)前日:4
* 當(dāng)前時(shí)/分/秒: 22/13/48
*/1.2 構(gòu)造一個(gè)指定年月日的時(shí)間
比如構(gòu)造:2019年8月30日18時(shí)26分30秒,大約是我對(duì)小方表白的時(shí)刻。
LocalDateTime specifiedTime = LocalDateTime.of(2019, Month.AUGUST, 30, 18, 26, 30);
System.out.println("構(gòu)造時(shí)間:" + specifiedTime );
/**
* 打印結(jié)果
*
* 構(gòu)造時(shí)間:2019-08-30T18:26:30
*/1.3 修改日期
LocalDateTime updateTime = LocalDateTime.now(); // 增加1個(gè)月 updateTime.plusMonths(1); // 減少2天 updateTime.minusDays(2); // 直接修改到2028年 updateTime.withYear(2028); // 直接修改到本月的第28天 updateTime.withDayOfMonth(28); // 組合條件修改 updateTime.withDayOfMonth(12).withYear(2060).minusDays(1);
1.4 格式化日期
LocalDateTime formatTime = LocalDateTime.now();
String type1 = formatTime.format(DateTimeFormatter.BASIC_ISO_DATE);
String type2 = formatTime.format(DateTimeFormatter.ISO_DATE);
String type3 = formatTime.format(DateTimeFormatter.ofPattern("yyyy-/-MM-/-dd"));
System.out.println("formatTime1:" + type1 +
"\nformatTime2: " + type2 +
"\nformatTime3: " + type3);
/**
* 輸出:
* formatTime1:20200904
* formatTime2: 2020-09-04
* formatTime3: 2020-/-09-/-04
*/1.5 計(jì)算時(shí)間差
Java 8 中有一個(gè)專(zhuān)門(mén)的類(lèi) Period 定義了日期間隔,通過(guò)Period.between 得到了兩個(gè)LocalDate 的差,返回的是兩個(gè)日期差幾年零幾月零幾天。如果希望得知兩個(gè)日期之間差幾天,直接調(diào)用 Period的getDays(). 方法得到的只是最后的“零幾天”,而不是算總的間隔天數(shù)。
LocalDate today = LocalDate.of(2020, 9, 5); LocalDate specifyDate = LocalDate.of(2019, 8, 30); System.out.println(Period.between(specifyDate, today).getDays()); System.out.println(Period.between(specifyDate, today)); System.out.println(ChronoUnit.DAYS.between(specifyDate, today)); /** * 輸出: * 6 * P1Y6D * 372 */
1.6 時(shí)間反解析
LocalDate inverseAnalysisTime = LocalDate.parse("2020-/-09-/-04" ,
DateTimeFormatter.ofPattern("yyyy-/-MM-/-dd"));
System.out.println("反解析后時(shí)間為:" + inverseAnalysisTime);
/**
* 輸出:
* 反解析后時(shí)間為:2020-09-04
*/
LocalDateTime inverseAnalysisTime = LocalDateTime.parse("2020-/-09-/-04 22:42" ,
DateTimeFormatter.ofPattern("yyyy-/-MM-/-dd HH:mm"));
System.out.println("反解析后時(shí)間為:" + inverseAnalysisTime);
/**
* 輸出:
* 反解析后時(shí)間為:2020-09-04T22:42
*/注意:
- 這里的
LocalDate、LocalTime和LocalDateTime的使用要區(qū)別好,不然解析過(guò)程會(huì)出現(xiàn)錯(cuò)誤。
1.7 Instant類(lèi)
Instant對(duì)象和時(shí)間戳是一一對(duì)應(yīng)的,它是精確到納秒的(而不是象舊版本的Date精確到毫秒)。
Instant instant = Instant.now(); System.out.println(instant); // 輸出, ISO-8601 標(biāo)準(zhǔn) // 2020-09-04T15:13:50.152933300Z
Instant 類(lèi)返回的值計(jì)算從 1970 年 1 月 1 日(1970-01-01T00:00:00Z)第一秒開(kāi)始的時(shí)間, 也稱(chēng)為 EPOCH。 發(fā)生在時(shí)期之前的瞬間具有負(fù)值,并且發(fā)生在時(shí)期后的瞬間具有正值 (1970-01-01T00:00:00Z 中的 Z 其實(shí)就是偏移量為 0)。Instant 類(lèi)提供的其他常量是 MIN, 表示最小可能(遠(yuǎn)遠(yuǎn))的瞬間,MAX表示最大(遠(yuǎn)期)瞬間。
- 該類(lèi)還提供了多種方法操作 Instant。加和減的增加或減少時(shí)間的方法。以下代碼將 1 小時(shí)添加到當(dāng)前時(shí)間:
Instant oneHourLater = Instant.now().plusHours(1);
- 比較時(shí)間的方法
long secondsFromEpoch = Instant.ofEpochSecond(0L).until(Instant.now(),ChronoUnit.SECONDS); // 1599233977 LocalDateTime start = LocalDateTime.of(2020, 9, 4, 0, 0, 0); LocalDateTime end = LocalDateTime.of(2020, 9, 8, 0, 0, 0); // 兩個(gè)時(shí)間之間相差了4天 System.out.println(start.until(end, ChronoUnit.DAYS)); // 4
- Instant 不包含年,月,日等單位。但是可以轉(zhuǎn)換成 LocalDateTime 或 ZonedDateTime, 如下 把一個(gè) Instant + 默認(rèn)時(shí)區(qū)轉(zhuǎn)換成一個(gè) LocalDateTime。
LocalDateTime ldt = LocalDateTime.ofInstant(Instant.now(), ZoneId.systemDefault());
System.out.printf("%s %d %d at %d:%d%n", ldt.getMonth(), ldt.getDayOfMonth(),
ldt.getYear(), ldt.getHour(), ldt.getMinute());
// SEPTEMBER 4 2020 at 23:40無(wú)論是 ZonedDateTime 或 OffsetTimeZone 對(duì)象可被轉(zhuǎn)換為 Instant 對(duì)象,因?yàn)槎加成涞綍r(shí)間軸上的確切時(shí)刻。 但是,相反情況并非如此。要將 Instant 對(duì)象轉(zhuǎn)換為 ZonedDateTime 或 OffsetDateTime 對(duì)象,需要提供時(shí)區(qū)或時(shí)區(qū)偏移信息。
二、線(xiàn)程安全性問(wèn)題
放兩張圖就一目了然:


三、數(shù)據(jù)庫(kù)中時(shí)間存儲(chǔ)
3.1 區(qū)別
int:
- 占用4個(gè)字節(jié)
- 建立索引之后,查詢(xún)速度快
- 條件范圍搜索可以使用使用between
- 不能使用mysql提供的時(shí)間函數(shù)
datetime:
- 占用8個(gè)字節(jié),允許為空值,可以自定義值
- 系統(tǒng)不會(huì)自動(dòng)修改其值
- 與時(shí)區(qū)無(wú)關(guān),存什么拿到的就是什么。
- 可以在指定
datetime字段的值的時(shí)候使用now()變量來(lái)自動(dòng)插入系統(tǒng)的當(dāng)前時(shí)間。
timestamp:
- 類(lèi)型在默認(rèn)情況下,insert、update 數(shù)據(jù)時(shí),
timestamp列會(huì)自動(dòng)以當(dāng)前時(shí)間(CURRENT_TIMESTAMP)填充/更新。 - 受時(shí)區(qū)timezone的影響以及MYSQL版本和服務(wù)器的SQL MODE的影響 ,存儲(chǔ)時(shí)對(duì)當(dāng)前的時(shí)區(qū)進(jìn)行轉(zhuǎn)換,檢索時(shí)再轉(zhuǎn)換回當(dāng)前的時(shí)區(qū)。
3.2 使用建議
int適合需要進(jìn)行大量時(shí)間范圍查詢(xún)的數(shù)據(jù)表。datetime適合用來(lái)記錄數(shù)據(jù)的原始的創(chuàng)建時(shí)間,因?yàn)闊o(wú)論你怎么更改記錄中其他字段的值,datetime字段的值都不會(huì)改變,除非你手動(dòng)更改它。timestamp適合用來(lái)記錄數(shù)據(jù)的最后修改時(shí)間,因?yàn)橹灰愀牧擞涗浿衅渌侄蔚闹担?code>timestamp字段的值都會(huì)被自動(dòng)更新。(如果需要可以設(shè)置timestamp不自動(dòng)更新)。
四、“老三樣”的坑
老三樣指:Date、Calender 和SimpleDateFormat。
4.1 初始化日期時(shí)間
如果要初始化一個(gè) 2020 年 9 月 5 日 11 點(diǎn) 12 分 13 秒這樣的時(shí)間:
Date date = new Date(2020, 9, 5, 11, 12, 13); // 輸出: // Tue Oct 05 11:12:13 CST 3920
這里就要注意:年應(yīng)該是和 1900 的差值,月應(yīng)該是從 0 到 11 而不是從 1 到 12。
我們也可以直接使用Calander:
Calendar calendar = Calendar.getInstance(); // 月份依舊是 0-11 calendar.set(2020,8,5,11,16,25); System.out.println(calendar.getTime()); // 輸出: // Sat Sep 05 11:16:25 CST 2020
4.2 時(shí)區(qū)問(wèn)題
關(guān)于 Date 類(lèi),我們要有兩點(diǎn)認(rèn)識(shí):
- Date 并無(wú)時(shí)區(qū)問(wèn)題,世界上任何一臺(tái)計(jì)算機(jī)使用 new Date() 初始化得到的時(shí)間都一樣。因?yàn)椋珼ate 中保存的是 UTC 時(shí)間,UTC 是以原子鐘為基礎(chǔ)的統(tǒng)一時(shí)間,不以太陽(yáng)參照計(jì)時(shí),并無(wú)時(shí)區(qū)劃分。
- Date 中保存的是一個(gè)時(shí)間戳,代表的是從 1970 年 1 月 1 日 0 點(diǎn)(Epoch 時(shí)間)到現(xiàn)在的毫秒數(shù)。嘗試輸出 Date(0):
System.out.println(new Date(0)); System.out.println(TimeZone.getDefault().getID() + ":" + TimeZone.getDefault().getRawOffset()/3600000); // 輸出: // Thu Jan 01 08:00:00 CST 1970 // 因?yàn)槲覚C(jī)器當(dāng)前的時(shí)區(qū)是中國(guó)上海,相比 UTC 時(shí)差 +8 小時(shí)。
對(duì)于國(guó)際化的項(xiàng)目,處理好時(shí)間和時(shí)區(qū)問(wèn)題首先就是要正確保存日期時(shí)間。這里有兩種保存方式:
- 方式一,以 UTC 保存,保存的時(shí)間沒(méi)有時(shí)區(qū)屬性,是不涉及時(shí)區(qū)時(shí)間差問(wèn)題的世界統(tǒng)一時(shí)間。我們通常說(shuō)的時(shí)間戳,或 Java 中的 Date 類(lèi)就是用的這種方式,這也是推薦的方式。
- 方式二,以字面量保存,比如年 / 月 / 日 時(shí): 分: 秒,一定要同時(shí)保存時(shí)區(qū)信息。只有有了時(shí)區(qū)信息,我們才能知道這個(gè)字面量時(shí)間真正的時(shí)間點(diǎn),否則它只是一個(gè)給人看的時(shí)間表示,只在當(dāng)前時(shí)區(qū)有意義。Calendar 是有時(shí)區(qū)概念的,所以我們通過(guò)不同的時(shí)區(qū)初始化 Calendar,得到了不同的時(shí)間。正確保存日期時(shí)間之后,就是正確展示,即我們要使用正確的時(shí)區(qū),把時(shí)間點(diǎn)展示為符合當(dāng)前時(shí)區(qū)的時(shí)間表示。
4.3 日期時(shí)間格式化和解析
每到年底,就有很多踩時(shí)間格式化的坑,比如“這明明是一個(gè) 2019 年的日期,怎么使用 SimpleDateFormat 格式化后就提前跨年了”。我們來(lái)重現(xiàn)一個(gè)這個(gè)問(wèn)題。
初始化一個(gè) Calendar,設(shè)置日期時(shí)間為 2019 年 12 月 29 日,使用大寫(xiě)的 YYYY 來(lái)初始化 SimpleDateFormat:
Locale.setDefault(Locale.SIMPLIFIED_CHINESE);
System.out.println("defaultLocale:" + Locale.getDefault());
Calendar calendar = Calendar.getInstance();
calendar.set(2019, Calendar.DECEMBER, 29,0,0,0);
SimpleDateFormat YYYY = new SimpleDateFormat("YYYY-MM-dd");
System.out.println("格式化: " + YYYY.format(calendar.getTime()));
System.out.println("weekYear:" + calendar.getWeekYear());
System.out.println("firstDayOfWeek:" + calendar.getFirstDayOfWeek());
System.out.println("minimalDaysInFirstWeek:" + calendar.getMinimalDaysInFirstWeek());
/**
* 輸出:
*
* defaultLocale:zh_CN
* 格式化: 2020-12-29
* weekYear:2020
* firstDayOfWeek:1
* minimalDaysInFirstWeek:1
*/更改時(shí)區(qū)試試:
Locale.setDefault(Locale.FRANCE); // 格式化: 2019-12-29 // weekYear:2019 // firstDayOfWeek:2 // minimalDaysInFirstWeek:4
那么 week year 就還是 2019 年,因?yàn)橐恢艿牡谝惶鞆闹芤婚_(kāi)始算,2020 年的第一周是 2019 年 12 月 30 日周一開(kāi)始,29 日還是屬于去年。JDK 的文檔中有說(shuō)明:小寫(xiě) y 是年,而大寫(xiě) Y 是 week year,也就是所在的周屬于哪一年,所以沒(méi)有特殊需求,針對(duì)年份的日期格式化,應(yīng)該一律使用 “y” 而非 “Y”。
另一個(gè)是:當(dāng)需要解析的字符串和格式不匹配的時(shí)候,SimpleDateFormat 表現(xiàn)得很寬容,還是能得到結(jié)果
String dateString = "20200905";
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMM");
try {
System.out.println("result:" + dateFormat.parse(dateString));
} catch (ParseException e) {
e.printStackTrace();
}
// 輸出:
// result:Sun May 01 00:00:00 CST 2095這里把0905當(dāng)初月份,往后推遲了905個(gè)月,但是并沒(méi)有爆出任何警告或錯(cuò)誤。
我們可以用Java8中的DateTimeFormatter代替:
String dateString = "20200905";
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMM");
System.out.println("result:" + dateTimeFormatter.parse(dateString));
// 控制臺(tái)報(bào)錯(cuò):
// Exception in thread "main" java.time.format.DateTimeParseException:Text '20200905' could not be parsed at index 0
// at java.base/java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:2046)
// at java.base/java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1874)
// at cn.litblue.datedemo.DateDemo.main(DateDemo.java:56)4.4 線(xiàn)程安全問(wèn)題

我們寫(xiě)一個(gè)案例:
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
ExecutorService threadPool = Executors.newFixedThreadPool(100);
for (int i = 0; i < 20; i++) {
//提交20個(gè)并發(fā)解析時(shí)間的任務(wù)到線(xiàn)程池,模擬并發(fā)環(huán)境
threadPool.execute(() -> {
for (int j = 0; j < 10; j++) {
try {
System.out.println(simpleDateFormat.parse("2020-09-05 12:10:30"));
} catch (ParseException e) {
e.printStackTrace();
}
}
});
}
threadPool.shutdown();
threadPool.awaitTermination(1, TimeUnit.HOURS);
運(yùn)行程序后大量報(bào)錯(cuò),且沒(méi)有報(bào)錯(cuò)的輸出結(jié)果也不正常。
五、總結(jié)
老三樣還是不要用了,新的日期時(shí)間類(lèi)不香么?

以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
前端與RabbitMQ實(shí)時(shí)消息推送未讀消息小紅點(diǎn)實(shí)現(xiàn)示例
這篇文章主要為大家介紹了前端與RabbitMQ實(shí)時(shí)消息推送未讀消息小紅點(diǎn)實(shí)現(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07
idea中項(xiàng)目前端網(wǎng)頁(yè)圖標(biāo)不顯示的原因及解決
這篇文章主要介紹了idea中項(xiàng)目前端網(wǎng)頁(yè)圖標(biāo)不顯示的原因及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-07-07
Java連接并操作Sedna XML數(shù)據(jù)庫(kù)的方法
這篇文章主要介紹了Java連接并操作Sedna XML數(shù)據(jù)庫(kù)的方法,較為詳細(xì)的說(shuō)明了Sedna XML數(shù)據(jù)庫(kù)的原理與功能,并給出了基于java操作Sedna XML數(shù)據(jù)庫(kù)的方法,需要的朋友可以參考下2015-06-06
springboot導(dǎo)出excel多個(gè)sheet導(dǎo)出的實(shí)現(xiàn)
在Java開(kāi)發(fā)過(guò)程中,合理配置pom.xml文件對(duì)項(xiàng)目的管理和構(gòu)建至關(guān)重要,通過(guò)添加依賴(lài)管理項(xiàng)目所需的庫(kù),簡(jiǎn)化了項(xiàng)目構(gòu)建過(guò)程,同時(shí),掌握導(dǎo)出excel工具類(lèi)的使用,可以有效地處理數(shù)據(jù)導(dǎo)出需求,提高工作效率,本文結(jié)合個(gè)人經(jīng)驗(yàn)2024-10-10
java實(shí)現(xiàn)拉鉤網(wǎng)上的FizzBuzzWhizz問(wèn)題示例
這篇文章主要介紹了java實(shí)現(xiàn)拉鉤網(wǎng)上的FizzBuzzWhizz問(wèn)題示例,需要的朋友可以參考下2014-05-05
Spring RedisTemplate 批量獲取值的2種方式小結(jié)
這篇文章主要介紹了Spring RedisTemplate 批量獲取值的2種方式小結(jié),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-06-06
java計(jì)算兩個(gè)時(shí)間相差天數(shù)的方法匯總
這篇文章主要介紹了java計(jì)算兩個(gè)時(shí)間相差天數(shù)的方法,感興趣的小伙伴們可以參考一下2015-11-11

