文章目錄
日期和時間是計算機處理的重要數據,在絕大多數軟件程序中,我們都要和日期和時間打交道。本篇文章我們將系統地學習 Java 對日期和時間的處理。(在這裏特別感謝廖雪峯大佬的文章,傳送門:廖雪峯 Java 教程-日期和時間,本篇文章參考了其文章中的資料,事實上,筆者並不認爲本文比廖大佬的文章更好,有時間的讀者可以直接閱讀原教程。)
一、時區
地球人都知道,我們地球是自西向東自轉的,所以東邊會比西邊早看到太陽,東邊的時間也總比西邊的快。如果全球採用統一的時間,比如都用北京時間,會產生什麼問題呢?
當正午十二點的太陽照射到北京時,身處地球另一面的紐約還是漆黑一片。對於紐約來說,日常作息時間就成了晚上九點開始上班,因爲那時太陽剛剛升起;所有紐約人都上班到第二天早上六點下班,因爲那時太陽剛剛落下。
雖然對於長期居住在一個地方的人來說,他可以適應自己本地的作息時間,但當他去其他地方旅遊或是與其他地方的人交流時,就必須查詢當地的作息時間,這會帶來很大的不便。
於是,在 1879 年,加拿大鐵路工程師弗萊明首次提出全世界按統一標準劃分“時區”。1884 年華盛頓子午線國際會議正式通過採納這種時區劃分,稱爲世界標準時制度。
時區劃分的初衷是儘量使中午貼近太陽上中天的時間,從此以後,各地的時間經過換算,都能統一地早上六點起牀,中午十二點午餐,晚上六點下班。
全球共分爲 24 個時區,所以每個時區佔 15˚ 經度。理論時區以能被 15 整除的經線爲中心,向東西兩側延伸 7.5˚。國際規定經過英國格林威治天文臺的那一條經線爲 0˚ 經線,這條經線也被稱作本初子午線。選擇格林威治既是因爲當初“日不落帝國”的強大,也是由於格林威治常年提供準確的航海觀測數據,19 世紀晚期,72% 的世界貿易都依靠以格林威治作爲本初子午線的航海圖表。
爲了避開國界線,有的時區的形狀並不規則,而是比較大的國家以國家內部行政分界線爲時區界線,這是實際時區,也稱爲法定時區。
身處地球的不同地區,時間可能是不同的,所以光靠時間我們無法確定一個時刻,要確定一個時刻必須要帶上時區。
表示時區有兩種常見的寫法,最常見的是 GMT,它的全稱是 Greenwich Mean Time,意思是格林威治標準時間,世界各地根據東西偏移量計算時區。比如,北京位於東八區,記做 GMT+8,紐約位於西五區,記做 GMT-5。
還有一種寫法是 UTC,它的全稱是 Coordinated Universal Time,意思是協調世界時,如果時間以 UTC 表示,則在時間後面直接加上一個“Z”(不加空格),“Z”是協調世界時中 0 時區的標誌。比如,“09:30 UTC” 寫作 “09:30Z” 或是 “0930Z”。“14:45:15 UTC” 寫作 “14:45:15Z” 或 “144515Z”。因爲在北約音標字母中用 “Zulu” 表示 “Z”,所以 UTC 時間也被稱做祖魯時間。
GMT 和 UTC 基本一樣,只不過 UTC 使用更加精確的原子鐘計時,每隔幾年會有一個閏秒。但我們無需關注兩者的差別,計算機在聯網時會自動與時間服務器同步時間。
計算不同時區的時間差很簡單,我們平時常用的北京時間位於東八區,即:GMT+8,它的值是在 GMT 的基礎上增加了 8 小時,紐約位於西五區,即:GMT-5,它的值是在 GMT 的基礎上減少了 5 小時。所以北京時間通常比紐約時間快 13 個小時。
我們現在知道,每往西越過一個時區,時間便提前一小時。據此我們來思考一個有趣的問題:如果我們一直往西,以每小時一個時區的速度前進,時間是否會靜止呢?
- 1.比如我們從北京出發,此時時間是 2020-2-11 8:00 GMT+8
- 2.當我們花費一個小時,走到東七區時,時間是 2020-2-11 8:00 GMT+7
- 3.當我們走到本初子午線時,時間是 2020-2-11 8:00 GMT
- 4.當我們走到西五區時,時間是 2020-2-11 8:00 GMT-5
- …
我們都知道地球是個球體,當我們繞地球一圈回到北京時,如果時間還是 2020-2-11 8:00 GMT+8,豈不是時間真的靜止了?進一步思考,如果我們以半小時一個時區的速度向西前進,豈不是時間還會倒流?
常識告訴我們,時間是不可能靜止也不可能倒流的。那麼這裏的問題出在哪裏呢?問題就出在東西時區的交界處。上文說到,地球分爲 24 個時區,包括標準時區、東一區~東十二區、西一區~西十二區。實際上,東十二區和西十二區是同一時區。
從 0˚ 經線開始,每往西跨一個時區時間便減少 1 小時,每往東跨一個時區便增加 1 小時。如此一來,到了另一端 180˚ 經線時,就會有 24 小時的落差,爲了平衡這一落差,人們規定由西向東越過此線日期需減少一天,由東向西越過此線時日期需增加一天。故而這一條線被稱之爲國際日期變更線,也叫換日線,它位於本初子午線的另一面。和時區界限類似,爲了避開國界線,換日線並不與 180˚ 經線重合,換日線實際上也是不規則的。
如果我們接着走下去:
- 5.當我們走到東 / 西十二區時,時間是 2020-2-11 8:00 GMT±12
- 6.我們越過國際換日線,日期增加一天,時間是 2020-2-12 8:00 GMT±12
- 7.當我們走到東十一區時,時間是 2020-2-12 8:00 GMT+11
- 8.當我們回到北京時,時間是 2020-2-12 8:00 GMT+8
此時,我們的環球之旅剛好用了 24 小時。
再來看一下如果我們以每半小時一個時區的速度向西行走,時間爲什麼不會逆流:
- 1.我們還是從北京出發,此時時間是 2020-2-11 8:00 GMT+8
- 2.當我們花費半小時,走到東七區時,時間是 2020-2-11 7:30 GMT+7
- 3.當我們走到本初子午線時,時間是 2020-2-11 4:00 GMT
- 4.當我們走到西五區時,時間是 2020-2-11 1:30 GMT-5
- 5.當我們走到東 / 西十二區時,時間是 2020-2-10 22:00 GMT±12
- 6.我們越過國際換日線,日期增加一天,時間是 2020-2-11 22:00 GMT±12
- 7.當我們走到東十一區時,時間是 2020-2-11 21:30 GMT+11
- 8.當我們回到北京時,時間是 2020-2-11 20:00 GMT+8
此時,我們的環球之旅剛好用了 12 小時。
二、夏令時
由於夏季和冬季白晝時間不一致,部分國家施行了夏令時制度,目的是讓人們根據白晝時間來調整作息。
夏令時:在夏天開始的時候,把時間往後撥 1 小時,夏天結束的時候,再把時間往前撥 1 小時。
施行夏令時使得人們可以儘量在白天工作,從而減少照明,節省電能。但夏令時也帶來了很多的不便,如夏令時開始和結束時,人們不得不調整睡眠時間;夏令時也使得時間計算變得複雜,在夏令時結束的當天,某些時間會出現兩次,容易造成交通、生產、會議安排等時間的混亂。中國曾經施行過一段時間夏令時,在 1992 年就被廢除了,而美國大部分地區現在還在使用夏令時。
美國使用夏令時時,紐約時間按照西四區計算,即:GMT-4。這段時間北京時間比紐約時間快 12 個小時,夏令時結束後,紐約時間又恢復到西五區 GMT-5。
由於各國規定有所差異,所以夏令時計算非常複雜。當我們需要計算夏令時時,應儘量使用 Java 庫提供的類,避免自己計算夏令時。
三、舊 API
Java 標準庫提供了兩套關於時間和日期的 API:
- 舊 API:位於 java.util 包中,裏面主要有 Date、Calendar、TimeZone 類
- 新 API:位於 java.time 包中,裏面主要有 LocalDateTime、ZonedDateTime、ZoneId 類
有兩套 API 的原因是舊 API 在設計時沒有考慮好時區問題,常量設計也有些不合理,導致使用起來不夠方便。新 API 很好的解決了這些問題。我們在開發時,除非維護老代碼,其他時候都應該儘量使用新 API。
3.1. Date
Date 類用於存儲日期和時間,查看其源碼可以發現,它保存了一個 long 類型的時間戳。時間戳是指格林威治時間從 1970 年 1 月 1 日零點到此刻經歷的秒數或毫秒數。
public class Date implements Serializable, Cloneable, Comparable<Date> {
private transient long fastTime;
...
}
Date 的基本用法如下:
import java.util.*;
public class Main {
public static void main(String[] args) {
// 獲取當前時間
Date date = new Date();
// 年份,需要加上 1900
System.out.println(date.getYear() + 1900);
// 月份,取值範圍是 0~11,代表 1~12 月,所以需要加上 1
System.out.println(date.getMonth() + 1);
// 日期,取值範圍是 1~31
System.out.println(date.getDate());
// 轉換爲 String,如:Tue Feb 11 17:24:10 CST 2020
System.out.println(date.toString());
// 轉換爲 GMT 時間,如:11 Feb 2020 09:24:10 GMT
System.out.println(date.toGMTString());
// 轉換爲本地化字符串,如:2020年2月11日 下午5:24:10
System.out.println(date.toLocaleString());
}
}
Date 在使用時有幾個缺點:
- 每次獲取年份、月份都需要轉換
- 只能獲取當前時區的時間,無法設置時區
- 無法加減日期和時間
- 無法計算某個月的第幾個星期幾
3.2. SimpleDateFormat
默認輸出的時間字符串的格式通常不能滿足我們的要求,所以我們需要用 SimpleDateFormat 來格式化輸出,它使用一些預定義的字符串表示格式化,較常用的字符串有:
- y:年
- M:月
- d:日
- H:小時
- m:分鐘
- s:秒
- S:毫秒
- a:上午 / 下午
- E:星期
- z:時區
附:Java 官網文檔中給出的預定義字符串表格
SimpleDateFormat 的使用:
import java.text.*;
import java.util.*;
public class Main {
public static void main(String[] args) {
Date date = new Date();
var formatter = new SimpleDateFormat("y-M-d a H:m:s.S E z");
// 輸出:2020-2-11 下午 17:26:13.776 週二 CST
System.out.println(formatter.format(date));
}
}
這裏的時區信息輸出爲 CST,表示 China Standard Time,也就是中國標準時間。
SimpleDateFormat 會根據預定義字符的長度打印不同長度的信息。以 M 爲例:
- M:輸出 2
- MM:輸出 02
- MMM:輸出 2月
- MMMM:輸出 二月
如果預定義字符串的長度短於需要輸出的信息,這時 Java 會輸出能包含全部信息的最短字符串
,也就是說 Java 不會丟棄任何信息,如上例中只用了一個 y,仍然輸出了 2020,並不會只輸出一個 2。
我們來發揮一下極客精神,探索一下預定義字符串過長 Java 會怎麼處理:
import java.text.*;
import java.util.*;
public class Main {
public static void main(String[] args) {
Date date = new Date();
var formatter = new SimpleDateFormat("yyyyyyyyyy-MMMMMMMMMM-dddddddddd aaaaaaaaaa HHHHHHHHHH:mmmmmmmmmm:ssssssssss.SSSSSSSSSS EEEEEEEEEE zzzzzzzzzz");
// 輸出:0000002020-二月-0000000011 下午 0000000017:0000000027:0000000008.0000000504 星期二 中國標準時間
System.out.println(formatter.format(date));
}
}
本例中,每個預定義字符的長度都爲 10,可以看到,系統對年、日、時、分、秒、毫秒的處理是用前置 0 補足位數,對月份、上午 / 下午、星期、時區的處理是輸出全中文。
SimpleDateFormat 可以設置時區,我們可以用 SimpleDateFormat 把 Date 獲取的時間轉換爲其他時區顯示出來:
import java.text.*;
import java.util.*;
public class Main {
public static void main(String[] args) {
Date date = new Date();
var formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss zzzzzz");
// 輸出:2020-02-11 17:27:33 中國標準時間
System.out.println(formatter.format(date));
formatter.setTimeZone(TimeZone.getTimeZone("America/New_York"));
// 輸出:2020-02-11 04:27:33 北美東部標準時間
System.out.println(formatter.format(date));
}
}
3.3. Calendar
舊 API 中,爲了加減日期和時間,Java 提供了 Calendar 類。
Calendar 的基本使用:
import java.util.*;
public class Main {
public static void main(String[] args) {
// 獲取當前時間
Calendar c = Calendar.getInstance();
// 年份,不必加 1900
int y = c.get(Calendar.YEAR);
// 月份,取值範圍是 0~11,代表 1~12 月,所以需要加上 1
int M = c.get(Calendar.MONTH) + 1;
// 日期,取值範圍是 1~31
int d = c.get(Calendar.DAY_OF_MONTH);
int H = c.get(Calendar.HOUR_OF_DAY);
int m = c.get(Calendar.MINUTE);
int s = c.get(Calendar.SECOND);
int S = c.get(Calendar.MILLISECOND);
// 星期,取值範圍 1~7,代表週日~週六
int E = c.get(Calendar.DAY_OF_WEEK);
// 輸出:2020-2-11 17:28:19.364 3
System.out.println(y + "-" + M + "-" + d + " " + H + ":" + m + ":" + s + "." + S + " " + E);
}
}
Calendar 修復了 Date 獲取年份時必須 + 1900 的問題,但月份仍然使用 0~11 表示 1~12 月,星期採用 1~7 表示週日~週六。雖然咱們程序員都從 0 開始計數,但日期和時間一般都是要展示給用戶看的,每次顯示時都要轉換實在是太不方便了,這也是需要新 API 的原因之一。
Calendar 提供的日期和時間的加減功能使用如下:
import java.text.*;
import java.util.*;
public class Main {
public static void main(String[] args) {
Calendar c = Calendar.getInstance();
var sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 當前時間:2020-02-11 17:28:56
System.out.println(sdf.format(c.getTime()));
// 加 2 天
c.add(Calendar.DAY_OF_MONTH, 2);
// 減 5 小時
c.add(Calendar.HOUR_OF_DAY, -5);
// 計算後的時間:2020-02-13 12:28:56
System.out.println(sdf.format(c.getTime()));
}
}
使用日期加減時有一點需要特別注意,我們來看一個例子:
import java.util.*;
public class Main {
public static void main(String[] args) {
Calendar c = Calendar.getInstance();
c.set(2020, 11, 31);
// 輸出:Thu Dec 31 17:09:11 CST 2020
System.out.println(c.getTime());
c.add(Calendar.MONTH, -1);
c.add(Calendar.MONTH, 1);
// 輸出:Wed Dec 30 17:09:11 CST 2020
System.out.println(c.getTime());
}
}
我們將 12 月 31 日減去 1 個月,再加上 1 個月,日期變成了 12 月 30 日!這是因爲 11 月 沒有 31 日,所以 12 月 31 日減去 1 個月時, Calendar 會自動將日期調整到 11 月 30 日,再加 1 個月,就變成了 12 月 30 日。也就是說Calendar 加減時,會根據月份自動調整日期
。
上文介紹 Date 時我們說到,單靠 Date 和 SimpleDateFormat 只能把本地時區的時間用其他時區顯示出來,無法自由的實現時區的轉換,比如我們身在中國,無法把紐約時間 GMT-5 轉換爲東京時間 GMT+9。但 Calendar 是可以設置時區的,所以我們現在有了一種間接轉換任意時區的方法:
import java.text.*;
import java.util.*;
public class Main {
public static void main(String[] args) {
Calendar c = Calendar.getInstance();
// 清空 Calendar 獲取到的本地時間
c.clear();
// 將時間重設爲:2020-2-11 13:00:00
c.set(2020, 1, 11, 13, 0, 0);
// 設定爲紐約時區
c.setTimeZone(TimeZone.getTimeZone("America/New_York"));
var sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss zzzzzz");
// c.getTime() 轉換成 Date 時,時間會轉換成本地時區
Date d = c.getTime();
// 輸出:2020-02-12 02:00:00 中國標準時間
System.out.println(sdf.format(d));
// 設置 SimpleDateFormat 的時區爲東京時區
sdf.setTimeZone(TimeZone.getTimeZone("Asia/Tokyo"));
// 輸出紐約時間轉換成的東京時間
// 輸出:2020-02-12 03:00:00 日本標準時間
System.out.println(sdf.format(d));
}
}
實際轉換過程爲:Calendar 保存的紐約時間
先轉換成 Date 保存的北京時間
,再用 SimpleDateFormat 將 Date 轉換成東京時間
展示出來。
上例中還可以看到,Calendar 使用 set 方法設置指定時間,除了此例中的一次性全部指定的方式外,也可以單個指定:
import java.util.*;
public class Main {
public static void main(String[] args) {
Calendar c = Calendar.getInstance();
// 輸出:Tue Feb 11 17:30:20 CST 2020
System.out.println(c.getTime());
c.set(Calendar.DAY_OF_MONTH, 9);
// 輸出:Sun Feb 09 17:30:20 CST 2020
System.out.println(c.getTime());
}
}
四、新 API
由於舊 API 存在的諸多不便,從 Java 8 開始,java.time 包提供了一套新的日期和時間的 API。主要有 LocalDateTime、ZonedDateTime、Instant、ZoneId、Duration、DateTimeFormatter。
新 API 不僅使用更方便,而且修正了 舊 API 中不合理的常量設計:
- 新 API 中,Month 取值範圍變成:1~12,表示 1~12月
- 新 API 中,Week 取值範圍變成:1~7,表示週一~週日
4.1. LocalDateTime
LocalDateTime 用來代替 Date 和 Calendar,LocalDateTime 的基本用法如下:
import java.time.*;
public class Main {
public static void main(String[] args) {
// 當前日期和時間
LocalDateTime dt = LocalDateTime.now();
// 嚴格按照 ISO 8601 格式打印,輸出:2020-02-11T17:31:27.027983
System.out.println(dt);
}
}
LocalDateTime 使用 now() 函數獲取當前日期和時間,輸出時嚴格按照 ISO 8601 格式打印。ISO 8601 是國際標準化組織的日期和時間的表示方法,全稱爲《數據存儲和交換形式·信息交換·日期和時間的表示方法》,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
我們可以通過 parse() 函數解析一個符合 ISO 8601 格式的字符串,創建出 LocalDateTime:
LocalDateTime dt = LocalDateTime.parse("2020-02-11T13:00:00");
除此之外,我們還可以通過 of() 函數指定日期和時間創建 LocalDateTime:
import java.time.*;
public class Main {
public static void main(String[] args) {
// 當前日期和時間,最後的 1 表示納秒,可不傳
LocalDateTime dt = LocalDateTime.of(2020, 2, 11, 13, 20, 30, 1);
// 輸出:2020-02-11T13:20:30.000000001
System.out.println(dt);
}
}
LocalDateTime 存儲了當前的日期信息和時間信息,如果我們只需要當前日期或當前時間,可以使用 LocalDate 和 LocalTime:
import java.time.*;
public class Main {
public static void main(String[] args) {
// 當前日期和時間
LocalDateTime dt = LocalDateTime.now();
LocalDate d = dt.toLocalDate();
LocalTime t = dt.toLocalTime();
// 輸出:2020-02-11
System.out.println(d);
// 輸出:17:32:09.742114
System.out.println(t);
}
}
同 LocalDateTime 類一樣,LocalDate 和 LocalTime 類也可以通過 now()、parse()、of() 方法創建。
LocalDateTime 在加減日期時,可以採用簡潔的鏈式調用:
import java.time.*;
public class Main {
public static void main(String[] args) {
LocalDateTime dt = LocalDateTime.of(2020, 2, 11, 13, 0, 0);
// 加 2 天,減 5 小時
LocalDateTime dt2 = dt.plusDays(2).minusHours(5);
// 輸出: 2020-02-13T08:00
System.out.println(dt2);
}
}
和 Calendar 一樣,LocalDateTime 在加減時,仍然會自動調整日期:
import java.time.*;
public class Main {
public static void main(String[] args) {
LocalDateTime dt = LocalDateTime.of(2020, 12, 31, 1, 0, 0);
// 輸出:2020-12-31T01:00
System.out.println(dt);
LocalDateTime dt2 = dt.minusMonths(1).plusMonths(1);
// 輸出:2020-12-30T01:00
System.out.println(dt2);
}
}
與 Calendar 不同的是,LocalDateTime 是不可變類,如此例中調用 minusMonths(1) 和 plusMonths(1) 方法後,dt 的值並沒有改變,這個函數返回的是一個調整後的新值,我們將這個新值賦值給了 dt2。
對應 Calendar 的 set() 方法,LocalDateTime 調整時間使用 withXxx() 方法:
- 調整年:withYear()
- 調整月:withMonth()
- 調整日:withDayOfMonth()
- 調整時:withHour()
- 調整分:withMinute()
- 調整秒:withSecond()
import java.time.LocalDateTime;
public class Main {
public static void main(String[] args) {
LocalDateTime dt = LocalDateTime.now();
// 輸出:2020-02-11T17:34:17.570764
System.out.println(dt);
LocalDateTime dt2 = dt.withDayOfMonth(9);
// 輸出:2020-02-09T17:34:17.570764
System.out.println(dt2);
}
}
LocalDateTime 還有一個 with() 方法允許我們做更復雜的運算:
import java.time.*;
import java.time.temporal.*;
public class Main {
public static void main(String[] args) {
// 今天 0:00
LocalDateTime startOfToday = LocalDate.now().atStartOfDay();
// 輸出:2020-02-11T00:00
System.out.println(startOfToday);
// 本月最後一天
LocalDate lastDay = LocalDate.now().with(TemporalAdjusters.lastDayOfMonth());
// 輸出:2020-02-29
System.out.println(lastDay);
// 下個月第一天
LocalDate firstDayOfNextMonth = LocalDate.now().with(TemporalAdjusters.firstDayOfNextMonth());
// 輸出:2020-03-01
System.out.println(firstDayOfNextMonth);
// 本月第1個週一
LocalDate firstWeekday = LocalDate.now().with(TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY));
// 輸出:2020-02-03
System.out.println(firstWeekday);
}
}
要比較兩個日期的先後,可以使用 LocalDateTime 的 isBefore()、isAfter() 方法:
import java.time.*;
public class Main {
public static void main(String[] args) {
LocalDateTime dt = LocalDateTime.now();
LocalDateTime dt2 = dt.plusDays(2);
// 輸出: true, true
System.out.println(dt.isBefore(dt2) + ", " + dt2.isAfter(dt));
}
}
4.2. ZonedDateTime
LocalDateTime 和 Date 類一樣,總是表示本地時區的時間,如果要表示帶時區的時間,需要使用 ZonedDateTime,它相當於 LocalDateTime + ZoneId。LocalDateTime 提供的方法,如 now()、of()、plusDays() 等,ZonedDateTime 也都提供。
ZonedDateTime 的使用:
import java.time.*;
public class Main {
public static void main(String[] args) {
ZonedDateTime date = ZonedDateTime.now();
ZonedDateTime dateEST = ZonedDateTime.now(ZoneId.of("America/New_York"));
// 輸出: 2020-02-11T17:35:27.191452+08:00[Asia/Shanghai]
System.out.println(date);
// 輸出: 2020-02-11T04:35:27.193329-05:00[America/New_York]
System.out.println(dateEST);
}
}
ZonedDateTime 通過 now() 函數獲取當前時區的時間,通過 now(ZoneId zone) 函數獲取指定時區的時間。這樣獲取到的兩個時間雖然時區不同,但表示的都是同一時刻(毫秒數不同是由於執行代碼會花費一點時間)。
通過給 LocalDateTime 設置 ZoneId,也可以創建出 ZonedDateTime:
import java.time.*;
public class Main {
public static void main(String[] args) {
LocalDateTime dt = LocalDateTime.now();
ZonedDateTime zdt = dt.atZone(ZoneId.systemDefault());
ZonedDateTime zdt2 = dt.atZone(ZoneId.of("America/New_York"));
// 輸出: 2020-02-11T17:35:50.246180+08:00[Asia/Shanghai]
System.out.println(zdt);
// 輸出: 2020-02-11T17:35:50.246180-05:00[America/New_York]
System.out.println(zdt2);
}
}
通過這種方式創建的 ZonedDateTime 日期和時間一樣,但時區不同,所以表示的是兩個不同時刻。
ZonedDateTime 可以通過 toLocalDateTime() 函數轉換成 LocalDateTime:
import java.time.*;
public class Main {
public static void main(String[] args) {
ZonedDateTime zdt = ZonedDateTime.now(ZoneId.of("America/New_York"));
LocalDateTime ldt = zdt.toLocalDateTime();
// 輸出: 2020-02-11T04:36:16.839591-05:00[America/New_York]
System.out.println(zdt);
// 輸出: 2020-02-11T04:36:16.839591
System.out.println(ldt);
}
}
我們看到,ZonedDateTime 轉換成 LocalDateTime 時,不會自動切換成本地時區的時間,而是直接丟棄時區信息。
由於 ZonedDateTime 自帶時區信息,所以在涉及時區轉換時使用 ZonedDateTime 非常方便。如上文中提到的將紐約時間轉換成的東京時間,使用 ZonedDateTime 實現如下:
import java.time.*;
public class Main {
public static void main(String[] args) {
ZonedDateTime zdt = ZonedDateTime.of(2020, 2, 11, 13, 0, 0, 0, ZoneId.of("America/New_York"));
// 使用 withZoneSameInstant() 方法切換時區
ZonedDateTime zdt2 = zdt.withZoneSameInstant(ZoneId.of("Asia/Tokyo"));
// 輸出: 2020-02-11T13:00-05:00[America/New_York]
System.out.println(zdt);
// 輸出: 2020-02-12T03:00+09:00[Asia/Tokyo]
System.out.println(zdt2);
}
}
4.3. DateTimeFormatter
上文已經說到,DateTimeFormatter 是用來代替 SimpleDateFormat 的。與 SimpleDateFormat 相比,DateTimeFormatter 的一個明顯優勢在於它是線程安全的。SimpleDateFormat 由於不是線程安全的,使用時只能在方法內部創建新的局部變量,而 DateTimeFormatter 可以只創建一個實例。
DataTimeFormat 預定義的字符串和 SimpleDateFormat 一模一樣,來看下 DateTimeFormatter 的基本使用:
import java.time.*;
import java.time.format.*;
public class Main {
public static void main(String[] args) {
ZonedDateTime date = ZonedDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
// 輸出: 2020-02-11 17:37
System.out.println(formatter.format(date));
}
}
還記得 LocalDateTime 的 parse() 方法嗎?我們查看一下它的源碼:
public static LocalDateTime parse(CharSequence text) {
return parse(text, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
}
public static LocalDateTime parse(CharSequence text, DateTimeFormatter
formatter) {
Objects.requireNonNull(formatter, "formatter");
return formatter.parse(text, LocalDateTime::from);
}
從源碼中我們看到,parse() 方法可以傳入兩個參數,第二個參數就是一個 DateTimeFormatter,也就是說不僅 ISO 8601 標準格式的字符串可以被解析,我們完全可以自定義被解析的字符串格式。
import java.time.*;
import java.time.format.*;
public class Main {
public static void main(String[] args) {
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
// 自定義格式化:2020/02/11 17:37:23
System.out.println(dtf.format(LocalDateTime.now()));
LocalDateTime dt2 = LocalDateTime.parse("2020/02/11 13:00:00", dtf);
// 用自定義格式解析:2020-02-11T13:00
System.out.println(dt2);
}
}
DataTimeFormatter 的 ofPattern() 方法還可以傳入一個 Locale 參數,這個參數的作用是使用當地的習慣來格式化時間:
import java.time.*;
import java.time.format.*;
import java.util.Locale;
public class Main {
public static void main(String[] args) {
ZonedDateTime zdt = ZonedDateTime.now();
var formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm ZZZZ");
// 輸出:2020-02-11T17:37 GMT+08:00
System.out.println(formatter.format(zdt));
var zhFormatter = DateTimeFormatter.ofPattern("yyyy MMM dd EE HH:mm", Locale.CHINA);
// 輸出:2020 2月 11 週二 17:37
System.out.println(zhFormatter.format(zdt));
var usFormatter = DateTimeFormatter.ofPattern("E, MMMM/dd/yyyy HH:mm", Locale.US);
// 輸出:Tue, February/11/2020 17:37
System.out.println(usFormatter.format(zdt));
}
}
4.4. Instant
在新 API 中,使用 Instant 表示時間戳,它類似於 System.currentTimeMillis()
。Instant 使用如下:
import java.time.*;
public class Main {
public static void main(String[] args) {
Instant now = Instant.now();
// UTC 標準時間,輸出:2020-02-11T09:38:13.891708Z
System.out.println(now);
// 以秒爲單位的時間戳, 輸出:1581413893
System.out.println(now.getEpochSecond());
// 以毫秒爲單位的時間戳,輸出:1581413893891
System.out.println(now.toEpochMilli());
}
}
給 Instant 加上一個時區,就可以創建出 ZonedDateTime:
import java.time.*;
public class Main {
public static void main(String[] args) {
Instant now = Instant.ofEpochSecond(1581413893);
ZonedDateTime zdt = now.atZone(ZoneId.systemDefault());
// GMT 標準時間,輸出:2020-02-11T17:38:13+08:00[Asia/Shanghai]
System.out.println(zdt);
}
}
五、新舊 API 的轉換
舊 API 轉新 API 可以通過 toInstant() 方法轉換爲 Instant,再由 Instant 轉換成 ZonedDateTime:
// Date -> Instant:
Instant ins1 = new Date().toInstant();
// Calendar -> Instant -> ZonedDateTime:
Calendar calendar = Calendar.getInstance();
Instant ins2 = Calendar.getInstance().toInstant();
ZonedDateTime zdt = ins2.atZone(calendar.getTimeZone().toZoneId());
新 API 轉舊 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);
以上,就是 Java 日期和時間的全部內容了,有什麼收穫或疑問歡迎在留言區一起交流。