原创:打码日记(微信公众号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 -Is2022-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/ShanghaiAsia/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 
时区写法ZoneIdTimeZone
+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/ShanghaiSystem.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,如下:

  1. 因为夏令时会将2点改成3点,导致2点没了,所以date命令报错了

    $ TZ='Asia/Shanghai' date -d 1986-05-04T02:00:00 +%sdate: invalid date ‘1986-05-04T02:00:00’$ TZ='Asia/Shanghai' date -d 1986-05-04T03:00:00 +%s515527200
  2. 工夫解析后再格式化输入,发现不一样了

    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
  3. 工夫加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/Shanghai1986-05-04 03:00:00 Asia/Shanghai  //加1小时,后果看起来加了2个小时1986-09-14 01:00:00 Asia/Shanghai1986-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:001986-05-04 03:00:00 Asia/Shanghai +09:001986-09-14 01:00:00 Asia/Shanghai +09:001986-09-14 01:00:00 Asia/Shanghai +08:00

如上,夏令时导致Asia/Shanghai这个时区不肯定是东8区了,也可能是东9区,故Java中,想将ZoneRegion转换为ZoneOffset,须要传递一个instant时刻参数,如下:

//输入+08:00Instant 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年后就没再履行了!