前提

最近刚好有新项目使用到JSR-310(JDK8)中引入的新日期API,打算做一下总结。本文编写基于JDK11,部分API可能是JDK9之后新增的。

地理知识补充

主要补充一下一些地理知识:时区、UTC、GMT、CST、DST和ISO-8601的相关概念。

时区

时区(Time Zone)是地球上的区域使用同一个时间定义。1884年在华盛顿召开国际经度会议时,为了克服时间上的混乱,规定将全球划分为24个时区。造成时间上的混乱是由于世界各个国家位于地球不同位置上,因此不同国家,特别是东西跨度大的国家日出、日落时间必定有所偏差(这个偏差我们通常叫做时差)。

前边提到全球共分为24个时区(东、西各12个时区),也就是每个时区的经度宽度为15度,其中本初子午线(0度经线)为0时区的中心线,而东、西12时区合并为一个时区,这些时区的经度分布如下:

时区 时区经度范围 时区中心线
UTC(0时区) 7.5°W~7.5°E
UTC+1(东1区) 7.5°E~22.5°E 15°E
UTC+2(东2区) 22.5°E~37.5°E 30°E
UTC+3(东3区) 37.5°E~52.5°E 45°E
UTC+4(东4区) 52.5°E~67.5°E 60°E
UTC+5(东5区) 67.5°E~82.5°E 75°E
UTC+6(东6区) 82.5°E~97.5°E 90°E
UTC+7(东7区) 97.5°E~112.5°E 105°E
UTC+8(东8区) 112.5°E~127.5°E 120°E
UTC+9(东9区) 127.5°E~142.5°E 135°E
UTC+10(东10区) 142.5°E~157.5°E 150°E
UTC+11(东11区) 157.5°E~172.5°E 165°E
UTC12(东、西12区) 172.5°E~172.5°W 180°
UTC-11(西11区) 172.5°W~157.5°W 165°W
UTC-10(西10区) 157.5°W~142.5°W 150°W
UTC-9(西9区) 142.5°W~127.5°W 135°W
UTC-8(西8区) 127.5°W~112.5°W 120°W
UTC-7(西7区) 112.5°W~97.5°W 105°W
UTC-6(西6区) 97.5°W~82.5°W 90°W
UTC-5(西5区) 82.5°W~67.5°W 75°W
UTC-4(西4区) 67.5°W~52.5°W 60°W
UTC-3(西3区) 52.5°W~37.5°W 45°W
UTC-2(西2区) 37.5°W~22.5°W 30°W
UTC-1(西1区) 22.5°W~7.5°W 15°W

但是实际上,通常1个国家或1个省份同时跨着多个时区,是因为为了照顾到行政上的方便,常将1个国家或1个省份划在同一个时区。例如,中国跨5个时区,但为了使用方便简单并且全国统一使用一个区时,实际上在中国使用东8区的区时一般称为北京时间作为标准时间。全球的标准时区划分如下:

Standard_World_Time_Zones

UTC、GMT、CST、DST与ISO-8601

GMT,Greenwich Mean Time,格林尼治(或者有时候翻译为格林威治)标准时间,是指位于伦敦郊区的皇家格林尼治天文台的标准时间。格林尼治所在地的标准时间也叫世界时UT。以地球自转为基础的时间计量系统。地球自转的角度可用地方子午线相对于地球上的基本参考点的运动来度量。为了测量地球自转,人们在地球上选取了两个基本参考点:春分点(见分至点)和平太阳,由此确定的时间分别称为恒星时和平太阳时。对于世界上发生的重大事件,都以格林尼治的地方时间记录下来。一旦知道了格林尼治时间,人们就很容易推算出相对应的本地时间。指位于英国伦敦郊区的皇家格林尼治天文台的标准时间,因为本初子午线被定义在通过那里的经线。 自1924年2月5日开始,格林尼治天文台每隔一小时会向全世界发放调时信息。 格林威治子午线上的地方时,或零时区(中时区)的区时叫做格林威治时间(又译为”格林尼治时间”),也叫”世界时”。原是采用格林威治的平正午作为一个平太阳日的开始,但在使用中有些不便。因此,国际天文学联合会于1928年决定,将由格林威治平子夜起算的平太阳时作为世界时,也就是通常所说的格林威治时间。格林威治时间所在时区为0时区,可以推算出使用GMT+8表示中国的时间,是因为中国位于东八区,时间上比格林威治时间快8个小时。

UTC,Coordinated Universal Time,也就是协调世界时,由于英文(CUT)和法文(TUC)的缩写不同,作为妥协,简称UTC。协调世界时是以原子时秒长为基础,在时刻上尽量接近于世界时的一种时间计量系统(由实验室用足够精确的铯原子钟导出的时间作为原子时,原子时的精确度极高,精度可以达到每2000万年才误差1秒)。国际原子时的准确度为每日数纳秒,而世界时的准确度为每日数毫秒。许多应用部门要求时间系统接近世界时UT,对于这种情况,一种称为协调世界时的折衷时标于1972年面世。为确保协调世界时与世界时相差不会超过0.9秒,在有需要的情况下会在协调世界时内加上正或负闰秒。因此协调世界时与国际原子时之间会出现若干整数秒的差别,两者之差逐年积累,便采用跳秒(闰秒)的方法使协调时与世界时的时刻相接近,其差不超过1s。通常将GMT和UTC视作等同,但UTC更加科学更加精确,它是以原子时为基础,在时刻上尽量接近世界时的一种时间计量系统。类似的,可以使用UTC+8表示中国的时间。

CST,China Standard Time,也就是中国标准时间,当格林威治时间为凌晨0:00时,中国标准时间正好为上午8:00,也就是CST实际上是参照于UTC,通用公式为:CST = UTC/GMT +8。

DST,Daylight Saving Time,阳光节约时,在我国称为夏时制,又称夏令时,是一种为节约能源而人为调整地方时间的制度。有些国家DST的使用时间较长,(如美国长达7个月)跨越了春夏秋等三个季节,因此简单地用夏时制的概念已经不能完全表达DST的确切含义了,所以有人也称其为节能时。所谓的DST,就是利用夏季天亮得早这一自然现象,人为地将时间提前一小时。这样就可以使人们早起早睡,以充分利用光照资源,减少照明时间,从而节约照明用电。目前中国已经弃用DST。

ISO-8601,是国际标准化组织的日期和时间的表示方法,全称为《数据存储和交换形式·信息交换·日期和时间的表示方法》。目前是2004年12月1日发行的第三版”ISO8601:2004”以替代1998年的第一版”ISO8601:1988”与2000年的第二版”ISO8601:2000”。该表示方法规定:年由4位数字组成YYYY,或者带正负号的四或五位数字表示±YYYYY,月、日用两位数字表示:MM、DD。只使用数字为基本格式。使用短横线”-“间隔开年、月、日为扩展格式。时间只使用数字为基本格式。使用冒号”:”间隔开小时、分、秒的为扩展格式。小时、分和秒都用2位数表示。合并表示时,要在时间前面加一大写字母T,如要表示北京时间2004年5月3日下午5点30分8秒,可以写成2004-05-03T17:30:08+08:00或20040503T173008+08。如果时间在零时区,并恰好与协调世界时相同,那么(不加空格地)在时间最后加一个大写字母Z。Z是相对协调世界时时间0偏移的代号。如下午2点30分5秒表示为14:30:05Z或143005Z;只表示小时和分,为1430Z或14:30Z;只表示小时,则为14Z或14Z。其他时区用实际时间加时差表示,当时的UTC+8时间表示为22:30:05+08:00或223005+0800,也可以简化成223005+08。Java中已存在的类java.util.Date默认就是使用ISO-8601表示的。

ZoneId

JSR-310中引入了抽象类java.time.ZoneId表示时区ID,它是旧APIjava.util.TimeZone的替代。ZoneRulesProvider用于加载Zone Rule(时区规则,ZoneRules),自定义实现是可以通过系统变量设置java.time.zone.DefaultZoneRulesProvider=全类名ZoneRulesProvider自定义的提供类,或者通过SPI加载,默认的实现类是TzdbZoneRulesProviderTzdbZoneRulesProvider会加载${JAVA_HONE}/lib/tzdb.dat文件(可以打开这个文件看下里面是怎么定义和描述时区的相应规则,这里不做详细分析,实际上如果深入了解这个规则文件的定义可以自行编写规则文件和实现加载类加载自定义的规则),这个DAT文件中存放着时区的规则映射。ZoneId就是时区ID,主要作用于各种时间API,以便于不同时区之间时间的转换以及计算。时区ID一共有两种不同的类型:

  • 固定时间偏移量(Fixed Offset) - 实际上对应ZoneOffset
  • 地理区域(Geographical Region) - 实际上对应ZoneRegion

静态方法ZoneId#of(String zoneId)会根据入参自动适配最终的时区ID到底表示固定时间偏移量还是地理区域,此方法支持如下的参数:

  • 地理区域参数,形式是:洲(州、国家)/城市,如ZoneId.of("Asia/Shanghia"),值得注意的是默认加载的规则里面没有北京。
  • 固定时间偏移量格式(offset-style),支持的格式比较多:
    • UTC或者GMT。
    • Z(相当于UTC)。
    • +h或者-h。
    • +hh或者-hh。
    • +hh:mm或者-hh:mm。
    • +hh:mm:ss或者-hh:mm:ss。
    • +hhmmss或者-hhmmss。

基于固定时间偏移量格式举几个例子:

ZoneId z;
z = ZoneId.of("Z"); //for UTC
z = ZoneId.of("+02:00");
z = ZoneId.of("-02:00");

ZoneId.of("GMT+2");
ZoneId.of("UTC");
ZoneId.of("UT+01:00");

ZoneId.of("Asia/Aden");
ZoneId.of("Etc/GMT+9");
ZoneId.of("Asia/Aqtau");

固定时间偏移量-ZoneOffset

java.time.ZoneOffsetjava.time.ZoneId实现类,表示固定时间偏移量,这个偏移量是以格林尼治(GMT)/协调世界时(UTC)为基准的偏移时间量。举个例子:

public class ZoneOffsetMain {

public static void main(String[] args) throws Exception {
ZoneOffset zoneOffset = ZoneOffset.of("+02:00");
System.out.println(zoneOffset);
zoneOffset = ZoneOffset.of("-02:00");
System.out.println(zoneOffset);
}
}

其中,ZoneOffset.of("+02:00")表示UTC下2小时的时间偏移(简单理解为东2区),ZoneOffset.of("-02:00")表示UTC下-2小时的时间偏移(简单理解为西2区)。

地理区域-ZoneRegion

java.time.ZoneRegionjava.time.ZoneId实现类(不过其修饰符为default,因此无法直接访问,只能通过ZoneId操作),表示地理区域,格式是:洲(州、国家)/城市。注释中提到:最常见的区域分类是时区数据库(TZDB),TZDB使用Europe/Paris’和’Asia/Tokyo’等形式区分地区。举个简单的例子:

public class ZoneRegionMain {

public static void main(String[] args) throws Exception {
ZoneId zoneId = ZoneId.systemDefault();
System.out.println(zoneId);
Set<String> availableZoneIds = ZoneId.getAvailableZoneIds();
for (String z : availableZoneIds) {
if (z.contains("Beijing") || z.contains("BeiJing")) {
System.out.println(z);
}
}
}
}

执行后控制台输出:

Asia/Shanghai

实际上,执行这个方法的时候,笔者在广州,得到的系统默认ZoneIdAsia/Shanghai,并且默认加载的地理区域中没有北京相关的ZoneId

小结

JSR-310中引入的时间API类ZoneId表示时区ID,具体有两种类型:固定时间偏移量-ZoneOffset和地理区域-ZoneRegion,这两种类型可以再细分为三种表示方式:

  • 地理区域表示,如:ZoneId.of("Asia/Aden")
  • GMT/UTC偏移量详细表示,如:ZoneId.of("UTC")ZoneId.of("GMT+2")
  • GMT/UTC偏移量简单表示,如:ZoneId.of("Z")ZoneId.of("+2:00")

参考资料:

(本文完 c-1-d e-a-20181223 r-a-20200302)