Java新舊時間日期API的使用和避坑指南
在 Java 8 之前,我們處理日期時間需求時,使用 Date、Calender 和 SimpleDateFormat,來聲明時間戳、使用日歷處理日期和格式化解析日期時間。但是,這些類的 API 的缺點比較明顯,比如可讀性差、易用性差、使用起來冗余繁瑣,還有線程安全問題。因此,Java 8 推出了新的日期時間類。
每一個類功能明確清晰、類之間協(xié)作簡單、API 定義清晰不踩坑,API 功能強大無需借助外部工具類即可完成操作,并且線程安全。
Java 8引入了三個新的日期時間類,分別是LocalDate、LocalTime和LocalDateTime,分別處理日期、時間和日期時間。
一、新的時間和日期API
1.1 獲取當(dāng)前時間
LocalDateTime localDateTime = LocalDateTime.now();
System.out.println("當(dāng)前時刻:" + localDateTime );
System.out.println("當(dāng)前年:" + localDateTime.getYear() +
"\n當(dāng)前月:" + localDateTime.getMonth() +
"\n當(dāng)前日:" + localDateTime.getDayOfMonth());
System.out.println("當(dāng)前時/分/秒:" +
localDateTime.getHour() +" / " +
localDateTime.getMinute() + "/" +
localDateTime.getSecond());
/*
* 打印結(jié)果
*
* 當(dāng)前時刻:2020-09-04T22:11:27.505361600
* 當(dāng)前年:2020
* 當(dāng)前月:SEPTEMBER
* 當(dāng)前日:4
* 當(dāng)前時/分/秒: 22/13/48
*/1.2 構(gòu)造一個指定年月日的時間
比如構(gòu)造:2019年8月30日18時26分30秒,大約是我對小方表白的時刻。
LocalDateTime specifiedTime = LocalDateTime.of(2019, Month.AUGUST, 30, 18, 26, 30);
System.out.println("構(gòu)造時間:" + specifiedTime );
/**
* 打印結(jié)果
*
* 構(gòu)造時間:2019-08-30T18:26:30
*/1.3 修改日期
LocalDateTime updateTime = LocalDateTime.now(); // 增加1個月 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 計算時間差
Java 8 中有一個專門的類 Period 定義了日期間隔,通過Period.between 得到了兩個LocalDate 的差,返回的是兩個日期差幾年零幾月零幾天。如果希望得知兩個日期之間差幾天,直接調(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 時間反解析
LocalDate inverseAnalysisTime = LocalDate.parse("2020-/-09-/-04" ,
DateTimeFormatter.ofPattern("yyyy-/-MM-/-dd"));
System.out.println("反解析后時間為:" + inverseAnalysisTime);
/**
* 輸出:
* 反解析后時間為:2020-09-04
*/
LocalDateTime inverseAnalysisTime = LocalDateTime.parse("2020-/-09-/-04 22:42" ,
DateTimeFormatter.ofPattern("yyyy-/-MM-/-dd HH:mm"));
System.out.println("反解析后時間為:" + inverseAnalysisTime);
/**
* 輸出:
* 反解析后時間為:2020-09-04T22:42
*/注意:
- 這里的
LocalDate、LocalTime和LocalDateTime的使用要區(qū)別好,不然解析過程會出現(xiàn)錯誤。
1.7 Instant類
Instant對象和時間戳是一一對應(yīng)的,它是精確到納秒的(而不是象舊版本的Date精確到毫秒)。
Instant instant = Instant.now(); System.out.println(instant); // 輸出, ISO-8601 標(biāo)準(zhǔn) // 2020-09-04T15:13:50.152933300Z
Instant 類返回的值計算從 1970 年 1 月 1 日(1970-01-01T00:00:00Z)第一秒開始的時間, 也稱為 EPOCH。 發(fā)生在時期之前的瞬間具有負(fù)值,并且發(fā)生在時期后的瞬間具有正值 (1970-01-01T00:00:00Z 中的 Z 其實就是偏移量為 0)。Instant 類提供的其他常量是 MIN, 表示最小可能(遠(yuǎn)遠(yuǎn))的瞬間,MAX表示最大(遠(yuǎn)期)瞬間。
- 該類還提供了多種方法操作 Instant。加和減的增加或減少時間的方法。以下代碼將 1 小時添加到當(dāng)前時間:
Instant oneHourLater = Instant.now().plusHours(1);
- 比較時間的方法
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); // 兩個時間之間相差了4天 System.out.println(start.until(end, ChronoUnit.DAYS)); // 4
- Instant 不包含年,月,日等單位。但是可以轉(zhuǎn)換成 LocalDateTime 或 ZonedDateTime, 如下 把一個 Instant + 默認(rèn)時區(qū)轉(zhuǎn)換成一個 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無論是 ZonedDateTime 或 OffsetTimeZone 對象可被轉(zhuǎn)換為 Instant 對象,因為都映射到時間軸上的確切時刻。 但是,相反情況并非如此。要將 Instant 對象轉(zhuǎn)換為 ZonedDateTime 或 OffsetDateTime 對象,需要提供時區(qū)或時區(qū)偏移信息。
二、線程安全性問題
放兩張圖就一目了然:


三、數(shù)據(jù)庫中時間存儲
3.1 區(qū)別
int:
- 占用4個字節(jié)
- 建立索引之后,查詢速度快
- 條件范圍搜索可以使用使用between
- 不能使用mysql提供的時間函數(shù)
datetime:
- 占用8個字節(jié),允許為空值,可以自定義值
- 系統(tǒng)不會自動修改其值
- 與時區(qū)無關(guān),存什么拿到的就是什么。
- 可以在指定
datetime字段的值的時候使用now()變量來自動插入系統(tǒng)的當(dāng)前時間。
timestamp:
- 類型在默認(rèn)情況下,insert、update 數(shù)據(jù)時,
timestamp列會自動以當(dāng)前時間(CURRENT_TIMESTAMP)填充/更新。 - 受時區(qū)timezone的影響以及MYSQL版本和服務(wù)器的SQL MODE的影響 ,存儲時對當(dāng)前的時區(qū)進行轉(zhuǎn)換,檢索時再轉(zhuǎn)換回當(dāng)前的時區(qū)。
3.2 使用建議
int適合需要進行大量時間范圍查詢的數(shù)據(jù)表。datetime適合用來記錄數(shù)據(jù)的原始的創(chuàng)建時間,因為無論你怎么更改記錄中其他字段的值,datetime字段的值都不會改變,除非你手動更改它。timestamp適合用來記錄數(shù)據(jù)的最后修改時間,因為只要你更改了記錄中其他字段的值,timestamp字段的值都會被自動更新。(如果需要可以設(shè)置timestamp不自動更新)。
四、“老三樣”的坑
老三樣指:Date、Calender 和SimpleDateFormat。
4.1 初始化日期時間
如果要初始化一個 2020 年 9 月 5 日 11 點 12 分 13 秒這樣的時間:
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 時區(qū)問題
關(guān)于 Date 類,我們要有兩點認(rèn)識:
- Date 并無時區(qū)問題,世界上任何一臺計算機使用 new Date() 初始化得到的時間都一樣。因為,Date 中保存的是 UTC 時間,UTC 是以原子鐘為基礎(chǔ)的統(tǒng)一時間,不以太陽參照計時,并無時區(qū)劃分。
- Date 中保存的是一個時間戳,代表的是從 1970 年 1 月 1 日 0 點(Epoch 時間)到現(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 // 因為我機器當(dāng)前的時區(qū)是中國上海,相比 UTC 時差 +8 小時。
對于國際化的項目,處理好時間和時區(qū)問題首先就是要正確保存日期時間。這里有兩種保存方式:
- 方式一,以 UTC 保存,保存的時間沒有時區(qū)屬性,是不涉及時區(qū)時間差問題的世界統(tǒng)一時間。我們通常說的時間戳,或 Java 中的 Date 類就是用的這種方式,這也是推薦的方式。
- 方式二,以字面量保存,比如年 / 月 / 日 時: 分: 秒,一定要同時保存時區(qū)信息。只有有了時區(qū)信息,我們才能知道這個字面量時間真正的時間點,否則它只是一個給人看的時間表示,只在當(dāng)前時區(qū)有意義。Calendar 是有時區(qū)概念的,所以我們通過不同的時區(qū)初始化 Calendar,得到了不同的時間。正確保存日期時間之后,就是正確展示,即我們要使用正確的時區(qū),把時間點展示為符合當(dāng)前時區(qū)的時間表示。
4.3 日期時間格式化和解析
每到年底,就有很多踩時間格式化的坑,比如“這明明是一個 2019 年的日期,怎么使用 SimpleDateFormat 格式化后就提前跨年了”。我們來重現(xiàn)一個這個問題。
初始化一個 Calendar,設(shè)置日期時間為 2019 年 12 月 29 日,使用大寫的 YYYY 來初始化 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
*/更改時區(qū)試試:
Locale.setDefault(Locale.FRANCE); // 格式化: 2019-12-29 // weekYear:2019 // firstDayOfWeek:2 // minimalDaysInFirstWeek:4
那么 week year 就還是 2019 年,因為一周的第一天從周一開始算,2020 年的第一周是 2019 年 12 月 30 日周一開始,29 日還是屬于去年。JDK 的文檔中有說明:小寫 y 是年,而大寫 Y 是 week year,也就是所在的周屬于哪一年,所以沒有特殊需求,針對年份的日期格式化,應(yīng)該一律使用 “y” 而非 “Y”。
另一個是:當(dāng)需要解析的字符串和格式不匹配的時候,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個月,但是并沒有爆出任何警告或錯誤。
我們可以用Java8中的DateTimeFormatter代替:
String dateString = "20200905";
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMM");
System.out.println("result:" + dateTimeFormatter.parse(dateString));
// 控制臺報錯:
// 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 線程安全問題

我們寫一個案例:
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
ExecutorService threadPool = Executors.newFixedThreadPool(100);
for (int i = 0; i < 20; i++) {
//提交20個并發(fā)解析時間的任務(wù)到線程池,模擬并發(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);
運行程序后大量報錯,且沒有報錯的輸出結(jié)果也不正常。
五、總結(jié)
老三樣還是不要用了,新的日期時間類不香么?

以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
IDEA項目的依賴(pom.xml文件)導(dǎo)入問題及解決
這篇文章主要介紹了IDEA項目的依賴(pom.xml文件)導(dǎo)入問題及解決,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08
mybatis如何通過接口查找對應(yīng)的mapper.xml及方法執(zhí)行詳解
這篇文章主要給大家介紹了利用mybatis如何通過接口查找對應(yīng)的mapper.xml及方法執(zhí)行的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家具有一定的參考學(xué)習(xí)價值,需要的朋友們下面跟著小編一起來學(xué)習(xí)學(xué)習(xí)吧。2017-06-06
Java基礎(chǔ)之SpringBoot整合knife4j
Swagger現(xiàn)在已經(jīng)成了最流行的接口文檔生成與管理工具,但是你是否在用的時候也在吐槽,它是真的不好看,接口測試的json數(shù)據(jù)沒法格式化,測試地址如果更改了還要去改配置,接口測試時增加token驗證是真的麻煩…針對Swagger的種種缺點,Knife4j就呼之欲出了.需要的朋友可以參考下2021-05-05
淺析java中ArrayList與Vector的區(qū)別以及HashMap與Hashtable的區(qū)別
以下是對java中ArrayList與Vector的區(qū)別以及HashMap與Hashtable的區(qū)別進行了詳細(xì)的解析。需要的朋友可以過來參考下2013-08-08
Java利用Jsoup解析和操作HTML的技術(shù)指南
在現(xiàn)代 Java 開發(fā)中,處理 HTML 數(shù)據(jù)是一項常見需求,無論是抓取網(wǎng)頁數(shù)據(jù)、解析 HTML 文檔,還是操作 DOM 樹,Jsoup 都是一個強大的工具,本文將介紹 Jsoup 的基本功能,并通過多個詳細(xì)的代碼示例展示如何使用它解析和操作 HTML,需要的朋友可以參考下2025-03-03
Mybatis讀取和存儲json類型數(shù)據(jù)的實現(xiàn)
本文主要介紹了Mybatis讀取和存儲json類型數(shù)據(jù)的實現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-06-06

