原创:打码日记(微信公众号 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 年后就没再履行了!