時區基本概念
時區(Time Zone)是指地球上的一個地區與格林尼治標準時間(GMT)或協調世界時(UTC)之間的時間差異。由於地球自轉的原因,不同的地理位置會有不同的時間。時區的劃分使得世界各地能夠更合理地安排時間,保持同步。
UTC(協調世界時): UTC 是一種標準時間,它沒有受到地球自轉速度變化影響,是全世界時間標準的基礎。所有的時區都是相對於 UTC 來定義的,例如 UTC+8 表示比 UTC 早 8 個小時的時間。
GMT(格林尼治標準時間): GMT 是一種基於地球自轉和太陽位置的時間標準,雖然它和 UTC 時間非常接近,但由於 GMT 是一種天文時間,存在微小的變化。
時區偏移(Offset): 時區通常會以 UTC 偏移的方式表示,例如 UTC+0:00、UTC+05:30、UTC-8 :00等等。UTC+8:00 的時區意味著這個時區比 UTC 早 8 個小時,比如中國的北京時間。
夏令時(Daylight Saving Time, DST): 某些國家和地區會使用夏令時,在夏季時將時間撥快一小時,以充分利用日照時間。夏令時的使用會導致某些地方的時間偏移在一年中有變化。
Java中的時區API
JDK8之前的TimeZone
/**
* 獲取所有可用的zoneId
*/
public static synchronized String[] getAvailableIDs();
/**
* 獲取所在地預設時區,國內便是Asia/Shanghai,東八區GMT+08:00
*/
public static TimeZone getDefault();
/**
* 根據ID獲取時區,注意如果是一個不支援的ID,則會降級到GMT時區,而不是報錯,因此有一個坑。
* 格式:
* 標準的地區名如Asia/Shanghai
* UTC、GMT
* 偏移量: GMT+08:00 (注意不支援UTC+08:00), 然後偏移量只支援到分鐘級別
* 這個API支援單獨的UTC,但是不支援UTC開頭的偏移量
* 因此如果你使用了UTC+08:00,則會降級到GMT時區,產生詭異現象。
*/
public static synchronized TimeZone getTimeZone(String ID);
以地區命名的id和偏移量區別
比如: Asia/Shanghai和GMT+08:00,對於大部分情況而言是對等的,然而由於夏令時的存在,使得時鐘撥快了一個小時,使用Asia/Shanghai的話,會根據規則判斷當前時間是不是在夏令時區間,從而計算出的偏移量是+09:00,而使用GMT+08:00則是固定的。因此使用Asia/Shanghai這個地區命名的不用使用者考慮這種繁瑣的夏令時規則。
JDK8新增的ZoneId、ZoneOffset
ZoneId
/**
* 獲取所有可用的zoneId
*/
public static Set<String> getAvailableZoneIds();
/**
* 獲取所在地預設時區,國內便是Asia/Shanghai,東八區UTC+08:00
*/
public static ZoneId systemDefault();
/**
* 根據ID獲取時區,這個方法做了改進,不支援的引數會直接報錯而不是降級到UTC,非常棒。
* 格式:
* 標準的地區名如Asia/Shanghai
* Z、UTC、GMT。這三個等價,相當於UTC+0
* 偏移量: UTC+08:00 GMT+08:00
* 這個API同時支援UTC和GMT字首
*/
public static ZoneId of(String zoneId);
ZoneOffset
代表UTC時區的偏移量,沒有了地區名寫法。ZoneOffset繼承了ZoneId
/**
* 覆蓋了ZoneId的of方法
* 格式: 不能寫字首,只能寫偏移量
* +08:00
* -08:00
* 特別的這個偏移量可以精確到秒
* +08:00:30
*/
public static ZoneOffset of(String offsetId);
/**
* 根據小時的偏移量
* ofHours(8)相當於+08:00
* ofHours(-8)相當於-08:00
*/
public static ZoneOffset ofHours(int hours);
ZoneOffset不僅僅提供了ofHours方法,諸如ofHoursMinutes等方法,一眼就懂。
新老時區API互轉
TimeZone提供了兩個方法
/**
* 靜態方法
*/
public static TimeZone getTimeZone(ZoneId zoneId);
/**
* 成員方法
*/
public ZoneId toZoneId();
紀元毫秒(epochMillis)
紀元毫秒是從計算機的紀元時間(Epoch time)開始經過的毫秒數。紀元時間通常被定義為 1970年1月1日 00:00:00 UTC。紀元毫秒通常用來表示計算機系統中日期和時間的時間戳。
注意是UTC時區的1970-01-01 00:00:00
給定一個毫秒數,它代表非UTC時區的具體哪個時間,必須要指定時區才有意義。
比如1000這個紀元毫秒數,即1秒
等同於UTC時區的1970-01-01 00:00:01
也等同於UTC+8:00時區的1970-01-01 08:00:01
想反的,給定一個時間格式字串,必須指定時區,才能轉成紀元毫秒數
java.util.Date
Date
類本身儲存的就是紀元毫秒數,getTime
方法就可以獲取到這個儲存的紀元毫秒數。
但是Date
類中的其它方法,諸如getYear
、getDate
、getSeconds
都是配合預設時區(中國是Asia/Shanghai)的一個時間來獲取的。
這些getXXX方法都已經被標註過時了。
Date
類中的toString
方法也一樣,作為顯示用,內部呼叫了這些getXXX方法。
使用DateFormat格式化成字元時,得到的結果具體是哪個時間,取決於DateFormat中設定的timeZone,若沒有指定,則預設值TimeZone.getDefault()
。
總結: Date類儲存的值是紀元毫秒數,與時區無關,不過內部中的一些方法返回值使用了預設時區來計算。
java.sql.Timestamp
java.sql.Timestamp
繼承了java.util.Date
,然後多加了一個nanos
屬性用來儲存納秒,值為[0 , 999,999,999]中的一個。也就是說增加了精度,其餘和java.util.Date
一樣。
Instant
Instant
為JDK8新增的一個類,可以用來代替java.util.Date
,內部儲存的是紀元納秒數,精度會更高。可以翻譯為時刻。
內部有兩個屬性
seconds:距離紀元時間的秒數
nanos:納秒數,[0, 999,999,999],用於提高精度。
建立方法
/**
* 根據毫秒數建立
*/
public static Instant ofEpochMilli(long epochMilli);
/**
* 一個引數是秒
* 第二個引數是納秒數,可以為正也可以為負,用來調整最終的一個結果
* 注意這兩個引數不等同於Instant中的連個成員變數seconds、nanos
*/
public static Instant ofEpochSecond(long epochSecond, long nanoAdjustment);
/**
* 當前時間對應的時刻
*/
public static Instant now();
基本使用
@Test
public void testInstant() {
// 該毫秒數對應UTC時間為2024-09-02 13:45:49
long millis = 1725284749641L;
Instant instant = Instant.ofEpochMilli(millis);
// 2024-09-02T13:45:49.641Z
System.out.println(instant);
instant = Instant.parse("2024-09-02T13:45:49.641Z");
// 1725284749641
System.out.println(instant.toEpochMilli());
// 配合時區轉成ZonedDateTime, 2024-09-02T21:45:49.641+08:00[Asia/Shanghai],可以看到時間加了8小時
System.out.println(instant.atZone(ZoneId.of("Asia/Shanghai")));
// 配合時區轉成OffsetDateTime,2024-09-02T21:45:49.641+08:00
System.out.println(instant.atOffset(ZoneOffset.ofHours(8)));
}
java.util.Date格式化
@Test
public void testFormatDate() {
// 該毫秒數對應UTC時間為2024-09-02 13:45:49
long millis = 1725284749641L;
Date date = new Date(millis);
String pattern = "yyyy-MM-dd HH:mm:ss";
/*
* SimpleDateFormat執行緒不安全, 方法會修改自身
* timeZone預設為TimeZone.getDefault(),國內即Asia/Shanghai
*/
DateFormat dateFormat = new SimpleDateFormat(pattern);
DateFormat dateFormatGmt9 = new SimpleDateFormat(pattern);
dateFormatGmt9.setTimeZone(TimeZone.getTimeZone("GMT+09:00"));
// 東八區: 2024-09-02 21:45:49
System.out.println(dateFormat.format(date));
// 東九區: 2024-09-02 22:45:49
System.out.println(dateFormatGmt9.format(date));
}
JSR310時間類格式化
@Test
public void testFormatJsr310Date() {
/*
* 下面4個都是代表東八區的2024-09-02 12:45:49
* LocalDateTime沒有時區概念
* Instant + 東八區便能得到時間024-09-02 12:45:49
*/
LocalDateTime localDateTime = LocalDateTime.of(2024,9, 2, 21, 45, 49);
ZonedDateTime zonedDateTime = ZonedDateTime.of(localDateTime, ZoneId.of("Asia/Shanghai"));
OffsetDateTime offsetDateTime = OffsetDateTime.of(localDateTime, ZoneOffset.ofHours(8));
Instant instant = Instant.ofEpochMilli(1725284749641L);
// 2024-09-02T21:45:49
System.out.println(localDateTime);
// 2024-09-02T21:45:49+08:00[Asia/Shanghai]
System.out.println(zonedDateTime);
// 2024-09-02T21:45:49+08:00
System.out.println(offsetDateTime);
/*
* 2024-09-02T13:45:49.641Z
* Instant toString方法展示的是UTC時區的時間, 末尾的Z代表UTC
* 那麼對應於東八區的時間就是2024-09-02T21:45:49.64
*/
System.out.println(instant);
/*
* DateTimeFormatter執行緒安全,和String一樣,為不可變物件
* 與SimpleDateFormat不同,時區欄位預設是null,沒有預設值
*/
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
// 2024-09-02 21:45:49
System.out.println(formatter.format(localDateTime));
// 2024-09-02 21:45:49
System.out.println(formatter.format(zonedDateTime));
// 2024-09-02 21:45:49
System.out.println(formatter.format(offsetDateTime));
// 會報錯, 格式化成年月日這樣形式, Formatter必須要指定時區
Assertions.assertThrowsExactly(UnsupportedTemporalTypeException.class, () -> formatter.format(instant));
// 指定東九區時區
DateTimeFormatter utc9Formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
.withZone(ZoneOffset.ofHours(9));
// 2024-09-02 21:45:49
System.out.println(utc9Formatter.format(localDateTime));
// 2024-09-02 22:45:49
System.out.println(utc9Formatter.format(zonedDateTime));
// 2024-09-02 22:45:49
System.out.println(utc9Formatter.format(offsetDateTime));
// 2024-09-02 22:45:49
System.out.println(utc9Formatter.format(instant));
}
總結:
DateTimeFormatter
是和String
一樣的不可變物件,執行緒安全。因此修改時一定記得要返回,否則無效哦。
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
formatter = formatter.withZone(ZoneOffset.ofHours(9));
DateTimeFormatter
中的時區屬性預設值為null,而不是系統所在時區,和SimpleDateFormat
不同。
LocalDateTime
格式化時不會進行時區轉換,因為本身沒有時區概念。
ZonedDateTime
、OffsetDateTime
格式化時,只有當DateTimeFormatter
明確指定了時區才會進行轉換。ZonedDateTime
、OffsetDateTime
攜帶的時區為源,DateTimeFormatter
中的時區為目標時區。
Instant
格式化時,DateTimeFormatter
必須指定時區,否則會報錯。畢竟紀元毫秒數(納秒數)搭配時區才能轉成時間。