原創:打碼日記(微信公眾號ID:codelogs),歡迎分享,轉載請保留出處。
簡介
最近在使用date命令時,發現表示東8區(中國時區)要使用GMT-8
,但在Java中卻需要使用GMT+8
,如下:
$ TZ='GMT-8' date -d@1647658144 +'%F %T %:z'
2022-03-19 10:49:04 +08:00
# 如果用GMT+8,反而慢了16小時
$ TZ='GMT+8' date -d@1647658144 +'%F %T %:z'
2022-03-18 18:49:04 -08:00
而在Java中,如下:
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss XXX");
String dateStr = dtf.format(Instant.ofEpochSecond(1647658144).atZone(ZoneId.of("GMT+8")));
System.out.println(dateStr);
//輸出2022-03-19 10:49:04 +08:00
這就讓人有點迷糊了,經過一段時間搜尋,發現在時區表達形式上還有不少知識點呢!
時區的偏移量表示法
眾所周知,為了方便各地區本地時間之間的轉換,人們將全球劃分為了24個時區,以格林尼治天文臺(GMT)為零時區,往東西兩個方向分別有12個時區,所以自然有了以GMT為字首的時區表示法,如下:
GMT+8
表示東8區,中國就是使用這個時區,而GMT-8
表示西8區,如果格林尼治天文臺的本地時間是2022-03-19的0點,那麼GMT+8
地區的本地時間就是2022-03-19的8點,而GMT-8
的本地時間就是往前8小時,即2022-03-18的16點。
注意,上面的各地區本地時間的表述雖然不同,但它們實際是同一個時刻(絕對時間),要理解本地時間與絕對時間的區別。
GMT+8
正是Java中支援的時區表示法,那為啥Linux中卻是GMT-8
呢?實際上Linux中的GMT-8
也可以寫成Etc/GMT-8
,這才是它的標準名稱,如下:
$ TZ='Etc/GMT-8' date -d@1647658144 -Is
2022-03-19T10:49:04+08:00
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss XXX");
String dateStr = dtf.format(Instant.ofEpochSecond(1647658144).atZone(ZoneId.of("Etc/GMT-8")));
System.out.println(dateStr);
//輸出2022-03-19 10:49:04 +08:00
可以發現用Etc/GMT-8
的話,Linux與Java的輸出都是一樣的了,是的,Etc/GMT-8
也是一種類似GMT+8
的時區表示機制,只不過它的+-
號是反的。
Ok,雖然上面的差異弄清楚了,但時區的表示形式還沒有介紹完,接著往下看...
除了GMT+8
表示方式外,我們還經常會看到UTC+8
這樣的表示方式,這是UTC時區表示法。
即生GMT何生UTC?這是由於GMT是以格林尼治天文臺為時間基準,但地球不是完美球體且自轉速度在變慢,所以地球自轉速度並不均勻,這導致以格林尼治天文臺為時間基準是不準的。
為了更準確度量時間,科學家們發明了UTC時間,以銫原子躍遷次數來度量時間,比GMT時間更準確,為了保證GMT的準確性,每隔幾年GMT時間會做一次調整,以與UTC時間對齊。
因此,既然有了更準確的UTC,那麼就有了以UTC為字首的時區表示法,如中國時區可使用UTC+8
。
各時區偏移量表示法一覽表,如下:
偏移量表示法 | 描述 |
---|---|
GMT+8 | 相對GMT多8個小時 |
Etc/GMT-8 | 同GMT+8,+- 號相反 |
UTC+8 | 同GMT+8 |
GMT+08:00 | 精確到分鐘級別 |
GMT+08:00:00 | 精確到秒級別 |
GMT+0800 | 精確到分鐘級別,省略冒號 |
GMT+080000 | 精確到秒級別,省略冒號 |
+08:00 | 精確到分鐘級別,省略字首 |
+08:00:00 | 精確到秒級別,省略字首 |
+0800 | 精確到分鐘級別,省略字首與冒號 |
+080000 | 精確到秒級別,省略字首與冒號 |
Z | 表示零時區,等同於GMT、UTC、GMT+0、UTC+0 |
時區的區域表示法
除了用偏移量來表示時區,為了方便,人們還按區域/城市的方式來定義時區,如Asia/Shanghai
,Asia/Hong_Kong
都表示東8區,具體有哪些城市命名的時區,可以在時區資料庫中檢視。
另外,為了簡化區域時區表示法,又定義了一套時區縮寫,如CST是中國時區China Standard Time
的縮寫,可以在時區縮寫中檢視各種縮寫定義。
注意,一般都不建議使用時區縮寫,因為時區縮寫的命名經常會重複,比如CST是Central Standard Time
(北美中部標準時間UTC -6)、China Standard Time
(中國標準時間UTC +8)、Cuba Standard Time
(古巴標準時間UTC -5)。
由於不同軟體對CST的解釋可能不同,導致會出現時間相差13或14個小時的情況,這在Java搭配MySQL時經常出現,我還專門寫了一篇文章mysql的timestamp會存在時區問題?,對於一定要使用時區縮寫的場景,可以使用香港時區縮寫HKT
,它不重複且和上海處於同一個時區。
區域表示法 | 描述 |
---|---|
Asia/Shanghai | 上海時區,即東8區 |
CST | 時區縮寫,慎用 |
Java中表示時區
在Java中和時區相關的類有TimeZone、ZoneId,其中TimeZone是老的時區類,而ZoneId是新的時區類,它有ZoneOffset和ZoneRegion兩個子類,分別代表偏移量表示法和區域表示法。
那它們都支援上述的哪些時區寫法呢?寫個Demo驗證一下,如下:
public static void main(String[] args) {
printZoneId("+08:00");
printZoneId("+0800");
printZoneId("GMT+8");
printZoneId("Etc/GMT-8");
printZoneId("UTC+8");
printZoneId("Asia/Shanghai");
printZoneId("CST");
printZoneId("Z");
}
public static void printZoneId(String zone){
ZoneId zoneId;
if(!ZoneId.SHORT_IDS.containsKey(zone)){
zoneId = ZoneId.of(zone);
}else{
zoneId = ZoneId.of(ZoneId.SHORT_IDS.get(zone));
}
TimeZone timeZone = TimeZone.getTimeZone(zone);
ZoneOffset zoneOffset = zoneId.getRules().getOffset(Instant.now());
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("xxx ZZZ O OOOO");
System.out.printf("%-14s -> %-28s -> class:%s -> TimeZone.offset:%d \n", zone, dtf.format(zoneOffset),
zoneId.getClass().getSimpleName(), timeZone.getRawOffset());
}
輸出如下:
+08:00 -> +08:00 +0800 GMT+8 GMT+08:00 -> class:ZoneOffset -> TimeZone.offset:0
+0800 -> +08:00 +0800 GMT+8 GMT+08:00 -> class:ZoneOffset -> TimeZone.offset:0
GMT+8 -> +08:00 +0800 GMT+8 GMT+08:00 -> class:ZoneRegion -> TimeZone.offset:28800000
Etc/GMT-8 -> +08:00 +0800 GMT+8 GMT+08:00 -> class:ZoneRegion -> TimeZone.offset:28800000
UTC+8 -> +08:00 +0800 GMT+8 GMT+08:00 -> class:ZoneRegion -> TimeZone.offset:0
Asia/Shanghai -> +08:00 +0800 GMT+8 GMT+08:00 -> class:ZoneRegion -> TimeZone.offset:28800000
CST -> -05:00 -0500 GMT-5 GMT-05:00 -> class:ZoneRegion -> TimeZone.offset:-21600000
Z -> +00:00 +0000 GMT GMT -> class:ZoneOffset -> TimeZone.offset:0
時區寫法 | ZoneId | TimeZone |
---|---|---|
+08:00 | 支援 | 不支援 |
+0800 | 支援 | 不支援 |
GMT+8 | 支援 | 支援 |
Etc/GMT-8 | 支援 | 支援 |
UTC+8 | 支援 | 不支援 |
Asia/Shanghai | 支援 | 支援 |
CST | 支援,代表北美西部時間,非中國標準時間 | 支援,代表北美西部時間,非中國標準時間 |
Z | 支援 | 支援 |
偏移量表示法與區域表示法區別
雖然偏移量表示法與區域表示法都可以表示時區,但由於夏令時的存在,它們並不完全等同。
夏令時(Daylight Saving Time: DST),也叫 夏時制,是指為了節約能源,在天亮的早的夏季,人為將時間調快一小時,以充分利用光照資源,節約照明用電。
而中國在 1986 年至 1991 年也實行過夏令時,在1986~1991的每年從四月中旬第一個星期日的凌晨2時整(北京時間),將時鐘撥快一小時,即將錶針由2時撥至3時,夏令時開始;到九月中旬第一個星期日的凌晨2時整(北京夏令時),再將時鐘撥回一小時,即將錶針由2時撥至1時,夏令時結束。從1986年到1991年的六個年度,除1986年因是實行夏時制的第一年,從5月4日開始到9月14日結束外,其它年份均按規定的時段施行。
故會有下面看起來有點奇怪的現象:
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss VV");
Instant instant = Instant.ofEpochSecond(515527200);
System.out.println(dtf.format(instant.atZone(ZoneId.of("Asia/Shanghai"))));
//輸出1986-05-04 03:00:00 Asia/Shanghai
System.out.println(dtf.format(instant.atZone(ZoneId.of("GMT+8"))));
//輸出1986-05-04 02:00:00 GMT+08:00
為什麼Asia/Shanghai
輸出為3點,而GMT+8
輸出為2點呢?原因是1986-05-04 02:00:00
這個時間點中國正開始實行夏令時,時鐘撥快了1小時。
而GMT+8
為什麼輸出為2點呢?因為中國、馬來西亞、菲律賓、新加坡的時區都是GMT+8
,只有中國在實行夏令時,而在GMT+8
中沒法感知到區域資訊,那java只能以沒有實行夏令時的方法來計算本地時間了。
夏令時導致的奇怪現象
正是由於夏令時的存在,導致程式可能出現詭異的現象甚至bug,如下:
- 由於夏令時會將2點改成3點,導致2點沒了,所以date命令報錯了
$ TZ='Asia/Shanghai' date -d 1986-05-04T02:00:00 +%s
date: invalid date ‘1986-05-04T02:00:00’
$ TZ='Asia/Shanghai' date -d 1986-05-04T03:00:00 +%s
515527200
- 時間解析後再格式化輸出,發現不一樣了
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss VV");
ZonedDateTime time1 = ZonedDateTime.parse("1986-05-04 02:00:00 Asia/Shanghai", dtf);
System.out.println(time1.format(dtf));
//輸出1986-05-04 03:00:00 Asia/Shanghai
- 時間加1小時,發現加了2小時或根本沒變
public static void main(String[] args) {
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss VV");
//加1小時剛好夏令時開始
ZonedDateTime time1 = ZonedDateTime.parse("1986-05-04 01:00:00 Asia/Shanghai", dtf);
printZonedDateTime(time1);
printZonedDateTime(time1.plusHours(1));
//加1小時剛好夏令時結束
ZonedDateTime time2 = ZonedDateTime.parse("1986-09-14 01:00:00 Asia/Shanghai", dtf);
printZonedDateTime(time2);
printZonedDateTime(time2.plusHours(1));
}
private static void printZonedDateTime(ZonedDateTime time){
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss VV");
System.out.println(time.format(dtf));
}
輸出如下:
1986-05-04 01:00:00 Asia/Shanghai
1986-05-04 03:00:00 Asia/Shanghai //加1小時,結果看起來加了2個小時
1986-09-14 01:00:00 Asia/Shanghai
1986-09-14 01:00:00 Asia/Shanghai //加1小時,結果時間看起來沒變
為啥會這樣呢?原因是本地時間雖然看起來沒變,但Asia/Shanghai
這個代表的時區卻發生了變化。
我們可以將上面printZonedDateTime
中時間格式由yyyy-MM-dd HH:mm:ss VV
修改為yyyy-MM-dd HH:mm:ss VV xxx
再執行,發現輸出如下:
1986-05-04 01:00:00 Asia/Shanghai +08:00
1986-05-04 03:00:00 Asia/Shanghai +09:00
1986-09-14 01:00:00 Asia/Shanghai +09:00
1986-09-14 01:00:00 Asia/Shanghai +08:00
如上,夏令時導致Asia/Shanghai
這個時區不一定是東8區了,也可能是東9區,故Java中,想將ZoneRegion轉換為ZoneOffset,需要傳遞一個instant時刻引數,如下:
//輸出+08:00
Instant instant = Instant.now();
System.out.println(ZoneId.of("Asia/Shanghai").getRules().getOffset(instant));
//輸出+09:00,在1986-05-04 02:00:00 +08:00處於夏令時,增加了1小時
Instant instant = Instant.ofEpochSecond(515527200);
System.out.println(ZoneId.of("Asia/Shanghai").getRules().getOffset(instant));
夏令時真是一種自欺欺人的做法,還好中國從1991年後就沒再實行了!