LocalDateTime、OffsetDateTime、ZonedDateTime互轉,這一篇絕對餵飽你
阿新 • • 發佈:2021-01-25
![](https://img-blog.csdnimg.cn/20210117101208962.png#pic_center)
# 前言
你好,我是A哥(YourBatman)。
在JSR 310日期時間體系了,一共有三個API可用於表示日期時間:
- LocalDateTime:本地日期時間
- OffsetDateTime:帶偏移量的日期時間
- ZonedDateTime:帶時區的日期時間
也許平時開發中你只用到過LocalDateTime這個API,那是極好的,但是不能止步於此,否則就圖樣圖森破了。
隨著場景的多樣性變化,咱們開發者接觸到OffsetDateTime/ZonedDateTime的概率越來越大,但凡和國際化產生上關係的大概率都會用得到它們。本文依然站在實用的角度,輔以具體程式碼示例,介紹它三。
## 本文提綱
![](https://img-blog.csdnimg.cn/20210120230412233.png#pic_center)
## 版本約定
- JDK:8
# 正文
下面這張圖是一個**完整**的日期時間,拆解各個部分的含義,一目瞭然(建議收藏此圖):
![](https://img-blog.csdnimg.cn/20210117101208962.png#pic_center)
因為LocalDate、LocalTime等理解起來比較簡單,就不用再花筆墨介紹了,重點放在LocalDateTime、OffsetDateTime、ZonedDateTime它三身上。
## 什麼是LocalDateTime?
![](https://img-blog.csdnimg.cn/20210117184204904.png#pic_center)
ISO-8601日曆系統中**不帶時區**的日期時間。
> 說明:ISO-8601日系統是現今世界上絕大部分國家/地區使用的,這就是我們國人所說的公曆,有閏年的特性
LocalDateTime是一個不可變的日期-時間物件,它表示一個日期時間,通常被視為**年-月-日-小時-分鐘-秒**。還可以訪問其他日期和時間欄位,如day-of-year、day-of-week和week-of-year等等,它的精度能達納秒級別。
該類不儲存時區,所以適合日期的描述,比如用於生日、deadline等等。但是請記住,如果沒有偏移量/時區等附加資訊,一個時間是**不能**表示時間線上的某一時刻的。
### 程式碼示例
**最大/最小值:**
```java
@Test
public void test1() {
LocalDateTime min = LocalDateTime.MIN;
LocalDateTime max = LocalDateTime.MAX;
System.out.println("LocalDateTime最小值:" + min);
System.out.println("LocalDateTime最大值:" + max);
System.out.println(min.getYear() + "-" + min.getMonthValue() + "-" + min.getDayOfMonth());
System.out.println(max.getYear() + "-" + max.getMonthValue() + "-" + max.getDayOfMonth());
}
輸出:
LocalDateTime最小值:-999999999-01-01T00:00
LocalDateTime最大值:+999999999-12-31T23:59:59.999999999
-999999999-1-1
999999999-12-31
```
**構造:**
```java
@Test
public void test2() {
System.out.println("當前時區的本地時間:" + LocalDateTime.now());
System.out.println("當前時區的本地時間:" + LocalDateTime.of(LocalDate.now(), LocalTime.now()));
System.out.println("紐約時區的本地時間:" + LocalDateTime.now(ZoneId.of("America/New_York")));
}
輸出:
當前時區的本地時間:2021-01-17T17:00:41.446
當前時區的本地時間:2021-01-17T17:00:41.447
紐約時區的本地時間:2021-01-17T04:00:41.450
```
注意,最後一個構造傳入了ZoneId,並不是說LocalDateTime和時區有關了,而是告訴說這個**Local指的是紐約**,細品這句話。
**計算:**
```java
@Test
public void test3() {
LocalDateTime now = LocalDateTime.now(ZoneId.systemDefault());
System.out.println("計算前:" + now);
// 加3天
LocalDateTime after = now.plusDays(3);
// 減4個小時
after = after.plusHours(-3); // 效果同now.minusDays(3);
System.out.println("計算後:" + after);
// 計算時間差
Period period = Period.between(now.toLocalDate(), after.toLocalDate());
System.out.println("相差天數:" + period.getDays());
Duration duration = Duration.between(now.toLocalTime(), after.toLocalTime());
System.out.println("相差小時數:" + duration.toHours());
}
輸出:
計算前:2021-01-17T17:10:15.381
計算後:2021-01-20T14:10:15.381
相差天數:3
相差小時數:-3
```
**格式化:**
```java
@Test
public void test4() {
LocalDateTime now = LocalDateTime.now(ZoneId.systemDefault());
// System.out.println("格式化輸出:" + DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(now));
System.out.println("格式化輸出(本地化輸出,中文環境):" + DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT, FormatStyle.SHORT).format(now));
String dateTimeStrParam = "2021-01-17 18:00:00";
System.out.println("解析後輸出:" + LocalDateTime.parse(dateTimeStrParam, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss", Locale.US)));
}
輸出:
格式化輸出(本地化輸出,中文環境):21-1-17 下午5:15
解析後輸出:2021-01-17T18:00
```
## 什麼是OffsetDateTime?
![](https://img-blog.csdnimg.cn/20210117192046856.png#pic_center)
ISO-8601日曆系統中與UTC偏移量有關的日期時間。OffsetDateTime是一個**帶有偏移量**的日期時間型別。儲存有精確到納秒的日期時間,以及偏移量。可以簡單理解為 OffsetDateTime = LocalDateTime + ZoneOffset。
![](https://img-blog.csdnimg.cn/20210117185734983.png#pic_center)
OffsetDateTime、ZonedDateTime和Instant它們三都能在時間線上以納秒精度儲存一個瞬間(請注意:LocalDateTime是不行的),也可理解我某個時刻。OffsetDateTime和Instant可用於模型的欄位型別,因為它們都表示瞬間值並且還不可變,所以適合網路傳輸或者資料庫持久化。
> ZonedDateTime不適合網路傳輸/持久化,因為即使同一個ZoneId時區,不同地方獲取到瞬時值也有可能不一樣
### 程式碼示例
**最大/最小值:**
```java
@Test
public void test5() {
OffsetDateTime min = OffsetDateTime.MIN;
OffsetDateTime max = OffsetDateTime.MAX;
System.out.println("OffsetDateTime最小值:" + min);
System.out.println("OffsetDateTime最大值:" + max);
System.out.println(min.getOffset() + ":" + min.getYear() + "-" + min.getMonthValue() + "-" + min.getDayOfMonth());
System.out.println(max.getOffset() + ":" + max.getYear() + "-" + max.getMonthValue() + "-" + max.getDayOfMonth());
}
輸出:
OffsetDateTime最小值:-999999999-01-01T00:00+18:00
OffsetDateTime最大值:+999999999-12-31T23:59:59.999999999-18:00
+18:00:-999999999-1-1
-18:00:999999999-12-31
```
偏移量的最大值是+18,最小值是-18,這是由ZoneOffset內部的限制決定的。
**構造:**
```java
@Test
public void test6() {
System.out.println("當前位置偏移量的本地時間:" + OffsetDateTime.now());
System.out.println("偏移量-4(紐約)的本地時間::" + OffsetDateTime.of(LocalDateTime.now(), ZoneOffset.of("-4")));
System.out.println("紐約時區的本地時間:" + OffsetDateTime.now(ZoneId.of("America/New_York")));
}
輸出:
當前位置偏移量的本地時間:2021-01-17T19:02:06.328+08:00
偏移量-4(紐約)的本地時間::2021-01-17T19:02:06.329-04:00
紐約時區的本地時間:2021-01-17T06:02:06.330-05:00
```
**計算:**
略
**格式化:**
```java
@Test
public void test7() {
OffsetDateTime now = OffsetDateTime.now(ZoneId.systemDefault());
System.out.println("格式化輸出(本地化輸出,中文環境):" + DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT, FormatStyle.SHORT).format(now));
String dateTimeStrParam = "2021-01-17T18:00:00+07:00";
System.out.println("解析後輸出:" + OffsetDateTime.parse(dateTimeStrParam));
}
輸出:
格式化輸出(本地化輸出,中文環境):21-1-17 下午7:06
解析後輸出:2021-01-17T18:00+07:00
```
**轉換:**
LocalDateTime -> OffsetDateTime
```java
@Test
public void test8() {
LocalDateTime localDateTime = LocalDateTime.of(2021, 01, 17, 18, 00, 00);
System.out.println("當前時區(北京)時間為:" + localDateTime);
// 轉換為偏移量為 -4的OffsetDateTime時間
// 1、-4地方的晚上18點
System.out.println("-4偏移量地方的晚上18點:" + OffsetDateTime.of(localDateTime, ZoneOffset.ofHours(-4)));
System.out.println("-4偏移量地方的晚上18點(方式二):" + localDateTime.atOffset(ZoneOffset.ofHours(-4)));
// 2、北京時間晚上18:00 對應的-4地方的時間點
System.out.println("當前地區對應的-4地方的時間:" + OffsetDateTime.ofInstant(localDateTime.toInstant(ZoneOffset.ofHours(8)), ZoneOffset.ofHours(-4)));
}
輸出:
當前時區(北京)時間為:2021-01-17T18:00
-4偏移量地方的晚上18點:2021-01-17T18:00-04:00
-4偏移量地方的晚上18點(方式二):2021-01-17T18:00-04:00
當前地區對應的-4地方的時間:2021-01-17T06:00-04:00
```
通過此例值得注意的是:`LocalDateTime#atOffset()/atZone()`只是增加了偏移量/時區,本地時間是並沒有改變的。若想實現本地時間到其它偏移量的**對應的**時間只能通過其`ofInstant()`系列構造方法。
OffsetDateTime -> LocalDateTime
```java
@Test
public void test81() {
OffsetDateTime offsetDateTime = OffsetDateTime.of(LocalDateTime.now(), ZoneOffset.ofHours(-4));
System.out.println("-4偏移量時間為:" + offsetDateTime);
// 轉為LocalDateTime 注意:時間還是未變的哦
System.out.println("LocalDateTime的表示形式:" + offsetDateTime.toLocalDateTime());
}
輸出:
-4偏移量時間為:2021-01-17T19:33:28.139-04:00
LocalDateTime的表示形式:2021-01-17T19:33:28.139
```
## 什麼是ZonedDateTime?
![](https://img-blog.csdnimg.cn/20210117192158682.png#pic_center)
ISO-8601國際標準日曆系統中**帶有時區**的日期時間。它儲存所有的日期和時間欄位,精度為納秒,以及一個時區,帶有用於處理不明確的本地日期時間的時區偏移量。
這個API可以處理從`LocalDateTime -> Instant -> ZonedDateTime`的轉換,其中用zone時區來表示偏移量(並非直接用offset哦)。兩個時間點之間的轉換會涉及到使用從ZoneId訪問的**規則計算**偏移量(換句話說:偏移量並非寫死而是根據規則計算出來的)。
獲取瞬間的偏移量很簡單,因為每個瞬間只有一個有效的偏移量。但是,獲取本地日期時間的偏移量並不簡單。存在這三種情況:
- 正常情況:有一個有效的偏移量。對於一年中的絕大多數時間,適用正常情況,即本地日期時間只有一個有效的偏移量
- 時間間隙情況:沒有有效偏移量。這是由於夏令時開始時從“冬季”改為“夏季”而導致時鐘向前撥的時候。**在間隙中**,沒有有效偏移量
- 重疊情況:有兩個有效偏移量。這是由於秋季夏令時從“夏季”到“冬季”的變化,時鐘會向後撥。在重疊部分中,有兩個有效偏移量
這三種情況如果要自己處理,估計頭都大了。這就是使用JSR 310的優勢,ZonedDateTime全幫你搞定,讓你使用無憂。
ZonedDateTime可簡單認為是**LocalDateTime和ZoneId的組合**。而ZoneOffset是其內建的動態計算出來的一個次要資訊,以確保輸出一個瞬時值而存在,畢竟在某個瞬間偏移量ZoneOffset肯定是確定的。ZonedDateTime也可以理解為儲存的狀態相當於三個獨立的物件:LocalDateTime、ZoneId和ZoneOffset。某個瞬間 = LocalDateTime + ZoneOffset。ZoneId確定了偏移量如何改變的規則。所以偏移量我們**並不能**自由設定(不提供set方法,構造時也不行),因為它由ZoneId來控制的。
![](https://img-blog.csdnimg.cn/20210117192246816.png#pic_center)
### 程式碼示例
**構造:**
```java
@Test
public void test9() {
System.out.println("當前位置偏移量的本地時間:" + ZonedDateTime.now());
System.out.println("紐約時區的本地時間:" + ZonedDateTime.of(LocalDateTime.now(), ZoneId.of("America/New_York")));
System.out.println("北京實現對應的紐約時區的本地時間:" + ZonedDateTime.now(ZoneId.of("America/New_York")));
}
輸出:
當前位置偏移量的本地時間:2021-01-17T19:25:10.520+08:00[Asia/Shanghai]
紐約時區的本地時間:2021-01-17T19:25:10.521-05:00[America/New_York]
北京實現對應的紐約時區的本地時間:2021-01-17T06:25:10.528-05:00[America/New_York]
```
**計算:**
略
**格式化:**
略
**轉換:**
LocalDateTime -> ZonedDateTime
```java
@Test
public void test10() {
LocalDateTime localDateTime = LocalDateTime.of(2021, 01, 17, 18, 00, 00);
System.out.println("當前時區(北京)時間為:" + localDateTime);
// 轉換為偏移量為 -4的OffsetDateTime時間
// 1、-4地方的晚上18點
System.out.println("紐約時區晚上18點:" + ZonedDateTime.of(localDateTime, ZoneId.of("America/New_York")));
System.out.println("紐約時區晚上18點(方式二):" + localDateTime.atZone(ZoneId.of("America/New_York")));
// 2、北京時間晚上18:00 對應的-4地方的時間點
System.out.println("北京地區此時間對應的紐約的時間:" + ZonedDateTime.ofInstant(localDateTime.toInstant(ZoneOffset.ofHours(8)), ZoneOffset.ofHours(-4)));
System.out.println("北京地區此時間對應的紐約的時間:" + ZonedDateTime.ofInstant(localDateTime, ZoneOffset.ofHours(8), ZoneOffset.ofHours(-4)));
}
輸出:
當前時區(北京)時間為:2021-01-17T18:00
紐約時區晚上18點:2021-01-17T18:00-05:00[America/New_York]
紐約時區晚上18點(方式二):2021-01-17T18:00-05:00[America/New_York]
北京地區此時間對應的紐約的時間:2021-01-17T06:00-04:00
北京地區此時間對應的紐約的時間:2021-01-17T06:00-04:00
```
OffsetDateTime -> ZonedDateTime
```java
@Test
public void test101() {
OffsetDateTime offsetDateTime = OffsetDateTime.of(LocalDateTime.now(), ZoneOffset.ofHours(-4));
System.out.println("-4偏移量時間為:" + offsetDateTime);
// 轉換為ZonedDateTime的表示形式
System.out.println("ZonedDateTime的表示形式:" + offsetDateTime.toZonedDateTime());
System.out.println("ZonedDateTime的表示形式:" + offsetDateTime.atZoneSameInstant(ZoneId.of("America/New_York")));
System.out.println("ZonedDateTime的表示形式:" + offsetDateTime.atZoneSimilarLocal(ZoneId.of("America/New_York")));
}
-4偏移量時間為:2021-01-17T19:43:28.320-04:00
ZonedDateTime的表示形式:2021-01-17T19:43:28.320-04:00
ZonedDateTime的表示形式:2021-01-17T18:43:28.320-05:00[America/New_York]
ZonedDateTime的表示形式:2021-01-17T19:43:28.320-05:00[America/New_York]
```
本例有值得關注的點:
- `atZoneSameInstant()`:將此日期時間與時區結合起來建立ZonedDateTime,以確保結果具有**相同的Instant**
- 所有偏移量-4 -> -5,時間點也從19 -> 18,確保了Instant保持一致嘛
- `atZoneSimilarLocal`:將此日期時間與時區結合起來建立ZonedDateTime,以確保結果具有**相同的本地時間**
- 所以直接效果和toLocalDateTime()是一樣的,但是它會盡可能的保留偏移量(所以你看-4變為了-5,保持了真實的偏移量)
我這裡貼出紐約2021年的夏令時時間區間:
![](https://img-blog.csdnimg.cn/20210117194528171.png#pic_center)
也就是說在2021.03.14 - 2021.11.07期間,紐約的偏移量是-4,其餘時候是-5。那麼再看這個例子(我把時間改為5月5號,也就是處於夏令營期間):
```java
@Test
public void test101() {
OffsetDateTime offsetDateTime = OffsetDateTime.of(LocalDateTime.of(2021, 05, 05, 18, 00, 00), ZoneOffset.ofHours(-4));
System.out.println("-4偏移量時間為:" + offsetDateTime);
// 轉換為ZonedDateTime的表示形式
System.out.println("ZonedDateTime的表示形式:" + offsetDateTime.toZonedDateTime());
System.out.println("ZonedDateTime的表示形式:" + offsetDateTime.atZoneSameInstant(ZoneId.of("America/New_York")));
System.out.println("ZonedDateTime的表示形式:" + offsetDateTime.atZoneSimilarLocal(ZoneId.of("America/New_York")));
}
輸出:
-4偏移量時間為:2021-05-05T18:00-04:00
ZonedDateTime的表示形式:2021-05-05T18:00-04:00
ZonedDateTime的表示形式:2021-05-05T18:00-04:00[America/New_York]
ZonedDateTime的表示形式:2021-05-05T18:00-04:00[America/New_York]
```
看到了吧,偏移量變為了-4。感受到夏令時的“威力”了吧。
## OffsetDateTime和ZonedDateTime的區別
LocalDateTime、OffsetDateTime、ZonedDateTime這三個哥們,LocalDateTime好理解,一般都沒有異議。但是很多同學對OffsetDateTime和ZonedDateTime傻傻分不清,這裡說說它倆的區別。
1. OffsetDateTime = LocalDateTime + 偏移量ZoneOffset;ZonedDateTime = LocalDateTime + 時區ZoneId
2. OffsetDateTime可以隨意設定偏移值,但ZonedDateTime無法自由設定偏移值,因為此值是由時區ZoneId控制的
3. **OffsetDateTime無法支援夏令時等規則,但ZonedDateTime可以很好的處理夏令時調整**
4. OffsetDateTime得益於不變性一般用於資料庫儲存、網路通訊;而ZonedDateTime得益於其時區特性,一般在指定時區裡顯示時間非常方便,無需認為干預規則
5. OffsetDateTime代表一個瞬時值,而ZonedDateTime的值是不穩定的,需要在某個瞬時根據當時的規則計算出來偏移量從而確定實際值
總的來說,OffsetDateTime和ZonedDateTime的區別主要在於ZoneOffset和ZoneId的區別。如果你只是用來傳遞資料,請使用OffsetDateTime,若你想在特定時區裡做時間顯示那麼請務必使用ZonedDateTime。
## 總結
本著拒絕淺嘗輒止的態度,深度剖析了很多同學可能不太熟悉的OffsetDateTime、ZonedDateTime兩個API。總而言之,想要真正掌握日期時間體系(不限於Java語言,而是所有語言,甚至日常生活),對時區、偏移量的瞭解是繞不過去的砍,這塊知識有所欠缺的朋友可往前翻翻補補課。
最後在使用它們三的過程中,有兩個提醒給你:
1. 所有日期/時間都是不可變的型別,所以若需要比較的話,請不要使用==,而是用equals()方法。
2、任何時候,構造一個日期時間(包括它們三)請永遠務必**顯示的指定時區**,哪怕是預設時區。這麼做的目的就是**明確程式碼的意圖**,消除語義上的不確定性。比如若沒指定時區,那到底是寫程式碼的人欠考慮了呢,還是就是想用預設時區呢?總之顯示指定絕大部分情況下比隱式“指定”語義上好得多。
## 本文思考題
**看完了不一定懂,看懂了不一定會**。來,文末3個思考題幫你覆盤:
1. 如何用LocalDateTime描述美國紐約本地時間?
2. OffsetDateTime和ZonedDateTime你到底該使用誰?
3. 一個人的生日應該用什麼Java型別儲存呢?
## 推薦閱讀
[GMT UTC CST ISO 夏令時 時間戳,都是些什麼鬼?](https://mp.weixin.qq.com/s/VdoQt88JfjPJTL9XgohZJQ)
[全網最全!徹底弄透Java處理GMT/UTC日期時間](https://mp.weixin.qq.com/s/R2OTG9xkUBUsd4xqQ1ND9A)
[全球城市ZoneId和UTC時間偏移量的最全對照表](https://mp.weixin.qq.com/s/TAoz0kx2LvvTAfPKh5Nwzg)
## 關注我
分享、成長,拒絕淺嘗輒止。關注【BAT的烏托邦】回覆關鍵字**專欄**有Spring技術棧、中介軟體等小而美的純原創專欄。本文已被 [https://www.yourbatman.cn](https://www.yourbatman.cn) 收錄。
本文所屬專欄:**JDK日期時間**,公號後臺回覆專欄名即可獲取全部內容。
A哥(YourBatman):Spring Framework/Boot開源貢獻者,Java架構師。非常注重**基本功修養**,相信底層基礎決定上層建築,堅實基礎才能煥發程式設計師更強生命力。文章特點為以小而美專欄形式重構知識體系,抽絲剝繭,致力於做人人能看懂的最好的專欄系列。可加我好友(**fsx1056342982**)共勉哦!
![](https://img-blog.csdnimg.cn/20210121074215630.gif#pic_center)
```java
System.out.println("點個贊吧!");
print_r('關注【BAT的烏托邦】!');
var_dump('點個贊吧!');
NSLog(@"關注【BAT的烏托邦】!");
console.log("點個贊吧!");
print("關注【BAT的烏托邦】!");
printf("點個贊吧!");
cout << "關注【BAT的烏托邦】!" << endl;
Console.WriteLine("點個贊吧!");
fmt.Println("關注【BAT的烏托邦】!");
Response.Write("點個贊吧!");
alert("關注【BAT的烏托邦】!");
echo("點個贊吧!");
```