作者:YourBatman
全网最全!彻底弄透Java处理GMT/UTC日期时间-腾讯云开发者社区-腾讯云
正文
上文铺了这么多概念,作为一枚Javaer最关心当然是这些“概念”在Java里的落地。平时工作中遇到时间如何处理?用Date还是JDK 8之后的日期时间API?如何解决跨时区转换等等头大问题。A哥向来管生管养,管杀管埋,因此本文就带你领略一下,Java是如何实现GMT和UTC的?
众所周知,JDK以版本8为界,有两套处理日期/时间的API:
在这里插入图片描述
虽然我一直鼓励弃用Date而支持在项目中只使用JSR 310日期时间类型,但是呢,由于Date依旧有庞大的存量用户,所以本文也不落单,对二者的实现均进行阐述。
Date类型实现
java.util.Date在JDK 1.0就已存在,用于表示日期 + 时间的类型,纵使年代已非常久远,并且此类的具有职责不单一,使用很不方便等诸多毛病,但由于十几二十年的历史原因存在,它的生命力依旧顽强,用户量巨大。
先来认识下Date,看下这个例子的输出:
@Test
public void test1() {
Date currDate = new Date();
System.out.println(currDate.toString());
// 已经@Deprecated
System.out.println(currDate.toLocaleString());
// 已经@Deprecated
System.out.println(currDate.toGMTString());
}
复制
运行程序,输出:
Fri Jan 15 10:22:34 CST 2021
2021-1-15 10:22:34
15 Jan 2021 02:22:34 GMT
复制
第一个:标准的UTC时间(CST就代表了偏移量 +0800) 第二个:本地时间,根据本地时区显示的时间格式 第三个:GTM时间,也就是格林威治这个时候的时间,可以看到它是凌晨2点(北京时间是上午10点哦)
第二个、第三个其实在JDK 1.1就都标记为@Deprecated过期了,基本禁止再使用。若需要转换为本地时间 or GTM时间输出的话,请使用格式化器java.text.DateFormat去处理。
时区/偏移量TimeZone
在JDK8之前,Java对时区和偏移量都是使用java.util.TimeZone
来表示的。
一般情况下,使用静态方法TimeZone#getDefault()
即可获得当前JVM所运行的时区,比如你在中国运行程序,这个方法返回的就是中国时区(也叫北京时区、北京时间)。
有的时候你需要做带时区的时间转换,譬如:接口返回值中既要有展示北京时间,也要展示纽约时间。这个时候就要获取到纽约的时区,以北京时间为基准在其上进行带时区转换一把:
@Test
public void test2() {
String patternStr = "yyyy-MM-dd HH:mm:ss";
// 北京时间(new出来就是默认时区的时间)
Date bjDate = new Date();
// 得到纽约的时区
TimeZone newYorkTimeZone = TimeZone.getTimeZone("America/New_York");
// 根据此时区 将北京时间转换为纽约的Date
DateFormat newYorkDateFormat = new SimpleDateFormat(patternStr);
newYorkDateFormat.setTimeZone(newYorkTimeZone);
System.out.println("这是北京时间:" + new SimpleDateFormat(patternStr).format(bjDate));
System.out.println("这是纽约时间:" + newYorkDateFormat.format(bjDate));
}
复制
运行程序,输出:
这是北京时间:2021-01-15 11:48:16
这是纽约时间:2021-01-14 22:48:16
复制
(11 + 24) - 22 = 13,北京比纽约快13个小时没毛病。
注意:两个时间表示的应该是同一时刻,也就是常说的时间戳值是相等的
那么问题来了,你怎么知道获取纽约的时区用America/New_York
这个zoneId呢?随便写个字符串行不行?
答案是当然不行,这是有章可循的。下面我介绍两种查阅zoneId的方式,任你挑选:
方式一:用Java程序把所有可用的zoneId打印出来,然后查阅
@Test
public void test3() {
String[] availableIDs = TimeZone.getAvailableIDs();
System.out.println("可用zoneId总数:" + availableIDs.length);
for (String zoneId : availableIDs) {
System.out.println(zoneId);
}
}
复制
运行程序,输出(大部分符合规律:/前表示所属州,/表示城市名称):
可用zoneId总数:628
Africa/Abidjan
Africa/Accra
...
Asia/Chongqing // 亚洲/重庆
Asia/Shanghai // 亚洲/上海
Asia/Dubai // 亚洲/迪拜
...
America/New_York // 美洲/纽约
America/Los_Angeles // 美洲/洛杉矶
...
Europe/London // 欧洲/伦敦
...
Etc/GMT
Etc/GMT+0
Etc/GMT+1
...
复制
值得注意的是并没有 Asia/Beijing 哦。
说明:此结果基于JDK 8版本,不同版本输出的总个数可能存在差异,但主流的ZoneId一般不会有变化
方式二: zoneId的列表是jre维护的一个文本文件,路径是你JDK/JRE的安装路径。地址在.\jre\lib目录的为未tzmappings
的文本文件里。打开这个文件去ctrl + f找也是可以达到查找的目的的。
这两种房子可以帮你找到ZoneId的字典方便查阅,但是还有这么一种情况:当前所在的城市呢,在tzmappings文件里根本没有(比如没有收录),那要获取这个地方的时间去显示怎么破呢?虽然概率很小,但不见得没有嘛,毕竟全球那么多国家那么多城市呢~
Java自然也考虑到了这一点,因此也是有办法的:指定其时区数字表示形式,其实也叫偏移量(不要告诉我这个地方的时区都不知道,那就真没救了),如下示例
@Test
public void test4() {
System.out.println(TimeZone.getTimeZone("GMT+08:00").getID());
System.out.println(TimeZone.getDefault().getID());
// 纽约时间
System.out.println(TimeZone.getTimeZone("GMT-05:00").getID());
System.out.println(TimeZone.getTimeZone("America/New_York").getID());
}
复制
运行程序,输出:
GMT+08:00 // 效果等同于Asia/Shanghai
Asia/Shanghai
GMT-05:00 // 效果等同于America/New_York
America/New_York
复制
值得注意的是,这里只能用GMT+08:00
,而不能用UTC+08:00
,原因下文有解释。
设置默认时区
一般来说,JVM在哪里跑,默认时区就是哪。对于国内程序员来讲,一般只会接触到东八区,也就是北京时间(本地时间)。随着国际合作越来越密切,很多时候需要日期时间国际化处理,举个很实际的例子:同一份应用在阿里云部署、在AWS(海外)上也部署一份供海外用户使用,此时同一份代码部署在不同的时区了,怎么破?
倘若时区不同,那么势必影响到程序的运行结果,很容易带来计算逻辑的错误,很可能就乱套了。Java让我们有多种方式可以手动设置/修改默认时区:
- API方式: 强制将时区设为北京时区
TimeZone.setDefault(TimeZone.getDefault().getTimeZone("GMT+8"));
- JVM参数方式:
-Duser.timezone=GMT+8
- 运维设置方式:将操作系统主机时区设置为北京时区,这是推荐方式,可以完全对开发者无感,也方便了运维统一管理
据我了解,很多公司在阿里云、腾讯云、国内外的云主机上部署应用时,全部都是采用运维设置统一时区:中国时区,这种方式来管理的,这样对程序来说就消除了默认时区不一致的问题,对开发者友好。
让人恼火的夏令时
你知道吗,中国曾经也使用过夏令时。
什么是夏令时?戳这里
离现在最近是1986年至1991年用过夏令时(每年4月中旬的第一个周日2时 - 9月中旬的第一个星期日2时止): 1986年5月4日至9月14日 1987年4月12日至9月13日 1988年4月10日至9月11日 1989年4月16日至9月17日 1990年4月15日至9月16日 1991年4月14日至9月15日
夏令时是一个“非常烦人”的东西,大大的增加了日期时间处理的复杂度。比如这个灵魂拷问:若你的出生日期是1988-09-11 00:00:00(夏令时最后一天)且存进了数据库,想一想,对此日期的格式化有没有可能就会出问题呢,有没有可能被你格式化成1988-09-10 23:00:00呢?
针对此拷问,我模拟了如下代码:
@Test
public void test5() throws ParseException {
String patterStr = "yyyy-MM-dd";
DateFormat dateFormat = new SimpleDateFormat(patterStr);
String birthdayStr = "1988-09-11";
// 字符串 -> Date -> 字符串
Date birthday = dateFormat.parse(birthdayStr);
long birthdayTimestamp = birthday.getTime();
System.out.println("老王的生日是:" + birthday);
System.out.println("老王的生日的时间戳是:" + birthdayTimestamp);
System.out.println("==============程序经过一番周转,我的同时 方法入参传来了生日的时间戳=============");
// 字符串 -> Date -> 时间戳 -> Date -> 字符串
birthday = new Date(birthdayTimestamp);
System.out.println("老王的生日是:" + birthday);
System.out.println("老王的生日的时间戳是:" + dateFormat.format(birthday));
}
复制
这段代码,在不同的JDK版本下运行,可能出现不同的结果,有兴趣的可copy过去自行试试。
关于JDK处理夏令时(特指中国的夏令时)确实出现过问题且造成过bug,当时对应的JDK版本是1.8.0_2xx
之前版本格式化那个日期出问题了,在这之后的版本貌似就没问题了。这里我提供的版本信息仅供参考,若有遇到类似case就升级JDK版本到最新吧,一般就不会有问题了。
发生这个情况是在JDK非常小的版本号之间,不太好定位精确版本号界限,所以仅供参考
总的来说,只要你使用的是较新版本的JDK,开发者是无需关心夏令时问题的,即使全球仍有很多国家在使用夏令时,咱们只需要面向时区做时间转换就没问题。
Date时区无关性
类Date表示一个特定的时间瞬间,精度为毫秒。既然表示的是瞬间/时刻,那它必然和时区是无关的,看下面代码:
@Test
public void test6() {
String patterStr = "yyyy-MM-dd HH:mm:ss";
Date currDate = new Date(System.currentTimeMillis());
// 北京时区
DateFormat bjDateFormat = new SimpleDateFormat(patterStr);
bjDateFormat.setTimeZone(TimeZone.getDefault());
// 纽约时区
DateFormat newYorkDateFormat = new SimpleDateFormat(patterStr);
newYorkDateFormat.setTimeZone(TimeZone.getTimeZone("America/New_York"));
// 伦敦时区
DateFormat londonDateFormat = new SimpleDateFormat(patterStr);
londonDateFormat.setTimeZone(TimeZone.getTimeZone("Europe/London"));
System.out.println("毫秒数:" + currDate.getTime() + ", 北京本地时间:" + bjDateFormat.format(currDate));
System.out.println("毫秒数:" + currDate.getTime() + ", 纽约本地时间:" + newYorkDateFormat.format(currDate));
System.out.println("毫秒数:" + currDate.getTime() + ", 伦敦本地时间:" + londonDateFormat.format(currDate));
}
复制
运行程序,输出:
毫秒数:1610696040244, 北京本地时间:2021-01-15 15:34:00
毫秒数:1610696040244, 纽约本地时间:2021-01-15 02:34:00
毫秒数:1610696040244, 伦敦本地时间:2021-01-15 07:34:00
复制
也就是说,同一个毫秒值,根据时区/偏移量的不同可以展示多地的时间,这就证明了Date它的时区无关性。
确切的说:Date对象里存的是自格林威治时间( GMT)1970年1月1日0点至Date所表示时刻所经过的毫秒数,是个数值。
读取字符串为Date类型
这是开发中极其常见的一种需求:client请求方扔给你一个字符串如"2021-01-15 18:00:00",然后你需要把它转为Date类型,怎么破?
问题来了,光秃秃的扔给我个字符串说是15号晚上6点时间,我咋知道你指的是北京的晚上6点,还是东京的晚上6点呢?还是纽约的晚上6点呢?
因此,对于字符串形式的日期时间,只有指定了时区才有意义。也就是说字符串 + 时区 才能精确知道它是什么时刻,否则是存在歧义的。
也许你可能会说了,自己平时开发中前端就是扔个字符串给我,然后我就给格式化为一个Date类型,并没有传入时区参数,运行这么久也没见出什么问题呀。如下所示:
@Test
public void test7() throws ParseException {
String patterStr = "yyyy-MM-dd HH:mm:ss";
// 模拟请求参数的时间字符串
String dateStrParam = "2020-01-15 18:00:00";
// 模拟服务端对此服务换转换为Date类型
DateFormat dateFormat = new SimpleDateFormat(patterStr);
System.out.println("格式化器用的时区是:" + dateFormat.getTimeZone().getID());
Date date = dateFormat.parse(dateStrParam);
System.out.println(date);
}
复制
运行程序,输出:
格式化器用的时区是:Asia/Shanghai
Wed Jan 15 18:00:00 CST 2020
复制
看起来结果没问题。事实上,这是因为默认情况下你们交互双发就达成了契约:双方均使用的是北京时间(时区),既然是相同时区,所以互通有无不会有任何问题。不信你把你接口给海外用户调试试?
对于格式化器来讲,虽然说编程过程中一般情况下我们并不需要给DateFormat设置时区(那就用默认时区呗)就可正常转换。但是作为高手的你必须清清楚楚,明明白白的知道这是由于交互双发默认有个相同时区的契约存在。
SimpleDateFormat格式化
Java中对Date类型的输入输出/格式化,推荐使用DateFormat而非用其toString()
方法。
DateFormat是一个时间格式化器抽象类,SimpleDateFormat是其具体实现类,用于以语言环境敏感的方式格式化和解析日期。它允许格式化(日期→文本)、解析(文本→日期)和规范化。
划重点:对语言环境敏感,也就是说对环境Locale、时区TimeZone都是敏感的。既然敏感,那就是可定制的
对于一个格式化器来讲,模式(模版)是其关键因素,了解一下:
日期/时间模式: 格式化的模式由指定的字符串组成,未加引号的大写/小写字母(A-Z a-z)代表特定模式,用来表示模式含义,若想原样输出可以用单引号’'包起来,除了英文字母其它均不解释原样输出/匹配。下面是它规定的模式字母(其它字母原样输出):
字母 | 含义 | 匹配类型 | 示例 |
---|---|---|---|
y | 年 | Year | 2020,20 |
M | 月 | Month | July; Jul; 07 |
d | 月中的天数(俗称日,最大值31) | Number | 10 |
H | 小时(0-23) | Number | 0,23 |
m | 分钟(0-59) | Number | 30,59 |
s | 秒(0-59) | Number | 30,59 |
— | — | — | yyyy-MM-dd HH:mm:ss(分隔符可以是任意字符,甚至汉字) |
Y | 当前周所在的年份 | Year | 2020(不建议使用,周若跨年有坑) |
S | 毫秒数(1-999) | Number | 999 |
a | am/pm | Text | PM |
z | 时区 | 通用时区 | Pacific Standard Time; PST; GMT-08:00 |
Z | 时区 | RFC 822时区 | -0800,+0800 |
X | 时区 | ISO 8601时区 | -08; -0800; -08:00 |
G | 年代 | Text | AD(公元)、BC(公元前) |
D | 年中的天数(1-366) | Number | 360 |
w | 年中的周数(1-54) | Number | 27 |
W | 月中的周数(1-5) | Number | 3 |
E | 星期几名称 | Text | Tuesday; Tue |
u | 星期几数字(1=Monday…) | Number | 1 |
k | 小时(1-24) | Number | 不建议使用 |
K/h | am/pm小时数字 | Number | 一般配合a一起使用 |
这个表格里出现了一些“特殊”的匹配类型,做如下解释:
- Text:格式化(Date -> String),如果模式字母的数目是4个或更多,则使用完整形式;否则,如果可能的话,使用简短或缩写形式。对于解析(String -> Date),这两种形式都一样,与模式字母的数量无关
@Test
public void test9() throws ParseException {
String patternStr = "G GG GGGGG E EE EEEEE a aa aaaaa";
Date currDate = new Date();
System.out.println("↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓中文地区模式↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓");
System.out.println("====================Date->String====================");
DateFormat dateFormat = new SimpleDateFormat(patternStr, Locale.CHINA);
System.out.println(dateFormat.format(currDate));
System.out.println("====================String->Date====================");
String dateStrParam = "公元 公元 公元 星期六 星期六 星期六 下午 下午 下午";
System.out.println(dateFormat.parse(dateStrParam));
System.out.println("↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓英文地区模式↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓");
System.out.println("====================Date->String====================");
dateFormat = new SimpleDateFormat(patternStr, Locale.US);
System.out.println(dateFormat.format(currDate));
System.out.println("====================String->Date====================");
dateStrParam = "AD ad bC Sat SatUrday sunDay PM PM Am";
System.out.println(dateFormat.parse(dateStrParam));
}
复制
运行程序,输出:
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓中文地区模式↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
====================Date->String====================
公元 公元 公元 星期六 星期六 星期六 下午 下午 下午
====================String->Date====================
Sat Jan 03 12:00:00 CST 1970
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓英文地区模式↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
====================Date->String====================
AD AD AD Sat Sat Saturday PM PM PM
====================String->Date====================
Sun Jan 01 00:00:00 CST 1970
复制
观察打印结果,除了符合模式规则外,还能在String -> Date解析时总结出两点结论:
- 英文单词,不分区大小写。如SatUrday sunDay都是没问题,但是不能有拼写错误
- 若有多个part表示一个意思,那么last win。如Sat SatUrday sunDay最后一个生效
对于Locale地域参数,因为中文不存在格式、缩写方面的特性,因此这些规则只对英文地域(如Locale.US生效)
- Number:格式化(Date -> String),模式字母的数量是数字的【最小】数量,较短的数字被零填充到这个数量。对于解析(String -> Date),模式字母的数量将被忽略,除非需要分隔两个相邻的字段
- Year:对于格式化和解析,如果模式字母的数量是4个或更多,则使用特定于日历的长格式。否则,使用日历特定的简短或缩写形式
- Month:如果模式字母的数量是3个或更多,则被解释为文本;否则,它将被解释为一个数字。
- 通用时区:如果该时区有名称,如Pacific Standard Time、PST、CST等那就用名称,否则就用GMT规则的字符串,如:GMT-08:00
- RFC 822时区:遵循RFC 822格式,向下兼容通用时区(名称部分除外)
- ISO 8601时区:对于格式化,如果与GMT的偏移值为0(也就是格林威治时间喽),则生成“Z”;如果模式字母的数量为1,则忽略小时的任何分数。例如,如果模式是“X”,时区是“GMT+05:30”,则生成“+05”。在进行解析时,“Z”被解析为UTC时区指示符。一般时区不被接受。如果模式字母的数量是4个或更多,在构造SimpleDateFormat或应用模式时抛出IllegalArgumentException。
- 这个规则理解起来还是比较费劲的,在开发中一般不太建议使用此种模式。若要使用请务必本地做好测试
SimpleDateFormat的使用很简单,重点是了解其规则模式。最后关于SimpleDateFormat的使用再强调这两点哈:
- SimpleDateFormat并非线程安全类,使用时请务必注意并发安全问题
- 若使用SimpleDateFormat去格式化成非本地区域(默认Locale)的话,那就必须在构造的时候就指定好,如Locale.US
- 对于Date类型的任何格式化、解析请统一使用SimpleDateFormat
JSR 310类型
曾经有个人做了个很有意思的投票,统计对Java API的不满意程度。最终Java Date/Calendar API斩获第二烂(第一烂是Java XML/DOM),体现出它烂的点较多,这里给你例举几项:
- 定义并不一致,在java.util和java.sql包中竟然都有Date类,而且呢对它进行格式化/解析类竟然又跑到java.text去了,精神分裂啊
- java.util.Date等类在建模日期的设计上行为不一致,缺陷明显。包括易变性、糟糕的偏移值、默认值、命名等等
- java.util.Date同时包含日期和时间,而其子类java.sql.Date却仅包含日期,这是什么神继承?
@Test
public void test10() {
long currMillis = System.currentTimeMillis();
java.util.Date date = new Date(currMillis);
java.sql.Date sqlDate = new java.sql.Date(currMillis);
java.sql.Time time = new Time(currMillis);
java.sql.Timestamp timestamp = new Timestamp(currMillis);
System.out.println("java.util.Date:" + date);
System.out.println("java.sql.Date:" + sqlDate);
System.out.println("java.sql.Time:" + time);
System.out.println("java.sql.Timestamp:" + timestamp);
}
复制
运行程序,输出:
java.util.Date:Sat Jan 16 21:50:36 CST 2021
java.sql.Date:2021-01-16
java.sql.Time:21:50:36
java.sql.Timestamp:2021-01-16 21:50:36.733
复制
- 国际化支持得并不是好,比如跨时区操作、夏令时等等
Java 自己也实在忍不了这么难用的日期时间API了,于是在2014年随着Java 8的发布引入了全新的JSR 310日期时间。JSR-310源于精品时间库joda-time打造,解决了上面提到的所有问题,是整个Java 8最大亮点之一。
JSR 310日期/时间 所有的 API都在java.time这个包内,没有例外。
在这里插入图片描述
当然喽,本文重点并不在于讨论JSR 310日期/时间体系,而是看看JSR 310日期时间类型是如何处理上面Date类型遇到的那些case的。
时区/偏移量ZoneId
在JDK 8之前,Java使用java.util.TimeZone
来表示时区。而在JDK 8里分别使用了ZoneId表示时区,ZoneOffset表示UTC的偏移量。
值得提前强调,时区和偏移量在概念和实际作用上是有较大区别的,主要体现在:
- UTC偏移量仅仅记录了偏移的小时分钟而已,除此之外无任何其它信息。举个例子:+08:00的意思是比UTC时间早8小时,没有地理/时区含义,相应的-03:30代表的意思仅仅是比UTC时间晚3个半小时
- 时区是特定于地区而言的,它和地理上的地区(包括规则)强绑定在一起。比如整个中国都叫东八区,纽约在西五区等等
中国没有夏令时,所有东八区对应的偏移量永远是+8;纽约有夏令时,因此它的偏移量可能是-4也可能是-5哦
综合来看,时区更好用。令人恼火的夏令时问题,若你使用UTC偏移量去表示那么就很麻烦,因为它可变:一年内的某些时期在原来基础上偏移量 +1,某些时期 -1;但若你使用ZoneId时区去表示就很方便喽,比如纽约是西五区,你在任何时候获取其当地时间都是能得到正确答案的,因为它内置了对夏令时规则的处理,也就是说啥时候+1啥时候-1时区自己门清,不需要API调用者关心。
UTC偏移量更像是一种写死偏移量数值的做法,这在天朝这种没有时区规则(没有夏令时)的国家不会存在问题,东八区和UTC+08:00效果永远一样。但在一些夏令时国家(如美国、法国等等),就只能根据时区去获取当地时间喽。所以当你不了解当地规则时,最好是使用时区而非偏移量。
ZoneId
它代表一个时区的ID,如Europe/Paris。它规定了一些规则可用于将一个Instant时间戳转换为本地日期/时间LocalDateTime。
上面说了时区ZoneId是包含有规则的,实际上描述偏移量何时以及如何变化的实际规则由java.time.zone.ZoneRules
定义。ZoneId则只是一个用于获取底层规则的ID。之所以采用这种方法,是因为规则是由政府定义的,并且经常变化,而ID是稳定的。
对于API调用者来说只需要使用这个ID(也就是ZoneId)即可,而需无关心更为底层的时区规则ZoneRules,和“政府”同步规则的事是它领域内的事就交给它喽。如:夏令时这条规则是由各国政府制定的,而且不同国家不同年一般都不一样,这个事就交由JDK底层的ZoneRules机制自行sync,使用者无需关心。
ZoneId在系统内是唯一的,它共包含三种类型的ID:
- 最简单的ID类型:ZoneOffset,它由’Z’和以’+‘或’-'开头的id组成。如:Z、+18:00、-18:00
- 另一种类型的ID是带有某种前缀形式的偏移样式ID,例如’GMT+2’或’UTC+01:00’。可识别的(合法的)前缀是’UTC’, ‘GMT’和’UT’
- 第三种类型是基于区域的ID(推荐使用)。基于区域的ID必须包含两个或多个字符,且不能以’UTC’、‘GMT’、‘UT’ '+‘或’-'开头。基于区域的id由配置定义好的,如Europe/Paris
概念说了一大推,下面给几个代码示例感受下吧。
1、获取系统默认的ZoneId:
@Test
public void test1() {
// JDK 1.8之前做法
System.out.println(TimeZone.getDefault());
// JDK 1.8之后做法
System.out.println(ZoneId.systemDefault());
}
输出:
Asia/Shanghai
sun.util.calendar.ZoneInfo[id="Asia/Shanghai",offset=28800000,dstSavings=0,useDaylight=false,transitions=29,lastRule=null]
复制
二者结果是一样的,都是Asia/Shanghai。因为ZoneId方法底层就是依赖TimeZone,如图:
在这里插入图片描述
在这里插入图片描述
2、指定字符串得到一个ZoneId:
@Test
public void test2() {
System.out.println(ZoneId.of("Asia/Shanghai"));
// 报错:java.time.zone.ZoneRulesException: Unknown time-zone ID: Asia/xxx
System.out.println(ZoneId.of("Asia/xxx"));
}
复制
很明显,这个字符串也是不能随便写的。那么问题来了,可写的有哪些呢?同样的ZoneId提供了API供你获取到所有可用的字符串id,有兴趣的同学建议自行尝试:
@Test
public void test3() {
ZoneId.getAvailableZoneIds();
}
复制
3、根据偏移量得到一个ZoneId:
@Test
public void test4() {
ZoneId zoneId = ZoneId.ofOffset("UTC", ZoneOffset.of("+8"));
System.out.println(zoneId);
// 必须是大写的Z
zoneId = ZoneId.ofOffset("UTC", ZoneOffset.of("Z"));
System.out.println(zoneId);
}
输出:
UTC+08:00
UTC
复制
这里第一个参数传的前缀,可用值为:“GMT”, “UTC”, or “UT”。当然还可以传空串,那就直接返回第二个参数ZoneOffset。若以上都不是就报错
注意:根据偏移量得到的ZoneId内部并无现成时区规则可用,因此对于有夏令营的国家转换可能出问题,一般不建议这么去做。
4、从日期里面获得时区:
@Test
public void test5() {
System.out.println(ZoneId.from(ZonedDateTime.now()));
System.out.println(ZoneId.from(ZoneOffset.of("+8")));
// 报错:java.time.DateTimeException: Unable to obtain ZoneId from TemporalAccessor:
System.out.println(ZoneId.from(LocalDateTime.now()));
System.out.println(ZoneId.from(LocalDate.now()));
}
复制
虽然方法入参是TemporalAccessor,但是只接受带时区的类型,LocalXXX是不行的,使用时稍加注意。
ZoneOffset
距离格林威治/UTC的时区偏移量,例如+02:00。值得注意的是它继承自ZoneId,所以也可当作一个ZoneId来使用的,当然并不建议你这么去做,请独立使用。
时区偏移量是时区与格林威治/UTC之间的时间差。这通常是固定的小时数和分钟数。世界不同的地区有不同的时区偏移量。在ZoneId类中捕获关于偏移量如何随一年的地点和时间而变化的规则(主要是夏令时规则),所以继承自ZoneId。
1、最小/最大偏移量:因为偏移量传入的是数字,这个是有限制的哦
@Test
public void test6() {
System.out.println("最小偏移量:" + ZoneOffset.MIN);
System.out.println("最小偏移量:" + ZoneOffset.MAX);
System.out.println("中心偏移量:" + ZoneOffset.UTC);
// 超出最大范围
System.out.println(ZoneOffset.of("+20"));
}
输出:
最小偏移量:-18:00
最小偏移量:+18:00
中心偏移量:Z
java.time.DateTimeException: Zone offset hours not in valid range: value 20 is not in the range -18 to 18
复制
2、通过时分秒构造偏移量(使用很方便,推荐):
@Test
public void test7() {
System.out.println(ZoneOffset.ofHours(8));
System.out.println(ZoneOffset.ofHoursMinutes(8, 8));
System.out.println(ZoneOffset.ofHoursMinutesSeconds(8, 8, 8));
System.out.println(ZoneOffset.ofHours(-5));
// 指定一个精确的秒数 获取实例(有时候也很有用处)
System.out.println(ZoneOffset.ofTotalSeconds(8 * 60 * 60));
}
// 输出:
+08:00
+08:08
+08:08:08
-05:00
+08:00
复制
看来,偏移量是能精确到秒的哈,只不过一般来说精确到分钟已经到顶了。
设置默认时区
ZoneId并没有提供设置默认时区的方法,但是通过文章可知ZoneId获取默认时区底层依赖的是TimeZone.getDefault()
方法,因此设置默认时区方式完全遵照TimeZone的方式即可(共三种方式,还记得吗?)。
让人恼火的夏令时
因为有夏令时规则的存在,让操作日期/时间的复杂度大大增加。但还好JDK尽量的屏蔽了这些规则对使用者的影响。因此:推荐使用时区(ZoneId)转换日期/时间,一般情况下不建议使用偏移量ZoneOffset去搞,这样就不会有夏令时的烦恼啦。
JSR 310时区相关性
java.util.Date类型它具有时区无关性,带来的弊端就是一旦涉及到国际化时间转换等需求时,使用Date来处理是很不方便的。
JSR 310解决了Date存在的一系列问题:对日期、时间进行了分开表示(LocalDate、LocalTime、LocalDateTime),对本地时间和带时区的时间进行了分开管理。LocalXXX表示本地时间,也就是说是当前JVM所在时区的时间;ZonedXXX表示是一个带有时区的日期时间,它们能非常方便的互相完成转换。
@Test
public void test8() {
// 本地日期/时间
System.out.println("================本地时间================");
System.out.println(LocalDate.now());
System.out.println(LocalTime.now());
System.out.println(LocalDateTime.now());
// 时区时间
System.out.println("================带时区的时间ZonedDateTime================");
System.out.println(ZonedDateTime.now()); // 使用系统时区
System.out.println(ZonedDateTime.now(ZoneId.of("America/New_York"))); // 自己指定时区
System.out.println(ZonedDateTime.now(Clock.systemUTC())); // 自己指定时区
System.out.println("================带时区的时间OffsetDateTime================");
System.out.println(OffsetDateTime.now()); // 使用系统时区
System.out.println(OffsetDateTime.now(ZoneId.of("America/New_York"))); // 自己指定时区
System.out.println(OffsetDateTime.now(Clock.systemUTC())); // 自己指定时区
}
复制
运行程序,输出:
================本地时间================
2021-01-17
09:18:40.703
2021-01-17T09:18:40.703
================带时区的时间ZonedDateTime================
2021-01-17T09:18:40.704+08:00[Asia/Shanghai]
2021-01-16T20:18:40.706-05:00[America/New_York]
2021-01-17T01:18:40.709Z
================带时区的时间OffsetDateTime================
2021-01-17T09:18:40.710+08:00
2021-01-16T20:18:40.710-05:00
2021-01-17T01:18:40.710Z
复制
本地时间的输出非常“干净”,可直接用于显示。带时区的时间显示了该时间代表的是哪个时区的时间,毕竟不指定时区的时间是没有任何意义的。LocalXXX因为它具有时区无关性,因此它不能代表一个瞬间/时刻。
另外,关于LocalDateTime、OffsetDateTime、ZonedDateTime三者的跨时区转换问题,以及它们的详解,因为内容过多放在了下文专文阐述,保持关注。
读取字符串为JSR 310类型
一个独立的日期时间类型字符串如2021-05-05T18:00-04:00它是没有任何意义的,因为没有时区无法确定它代表那个瞬间,这是理论当然也适合JSR 310类型喽。
遇到一个日期时间格式字符串,要解析它一般有这两种情况:
- 不带时区/偏移量的字符串:要么不理它说转换不了,要么就约定一个时区(一般用系统默认时区),使用LocalDateTime来解析
@Test
public void test11() {
String dateTimeStrParam = "2021-05-05T18:00";
LocalDateTime localDateTime = LocalDateTime.parse(dateTimeStrParam);
System.out.println("解析后:" + localDateTime);
}
输出:
解析后:2021-05-05T18:00
复制
- 带时区字/偏移量的符串:
@Test
public void test12() {
// 带偏移量 使用OffsetDateTime
String dateTimeStrParam = "2021-05-05T18:00-04:00";
OffsetDateTime offsetDateTime = OffsetDateTime.parse(dateTimeStrParam);
System.out.println("带偏移量解析后:" + offsetDateTime);
// 带时区 使用ZonedDateTime
dateTimeStrParam = "2021-05-05T18:00-05:00[America/New_York]";
ZonedDateTime zonedDateTime = ZonedDateTime.parse(dateTimeStrParam);
System.out.println("带时区解析后:" + zonedDateTime);
}
输出:
带偏移量解析后:2021-05-05T18:00-04:00
带时区解析后:2021-05-05T18:00-04:00[America/New_York]
复制
请注意带时区解析后这个结果:字符串参数偏移量明明是-05,为毛转换为ZonedDateTime后偏移量成为了-04呢???
这里是我故意造了这么一个case引起你的重视,对此结果我做如下解释:
在这里插入图片描述
如图,在2021.03.14 - 2021.11.07期间,纽约的偏移量是-4,其余时候是-5。本例的日期是2021-05-05处在夏令时之中,因此偏移量是-4,这就解释了为何你显示的写了-5最终还是成了-4。
JSR 310格式化
针对JSR 310日期时间类型的格式化/解析,有个专门的类java.time.format.DateTimeFormatter
用于处理。
DateTimeFormatter也是一个不可变的类,所以是线程安全的,比SimpleDateFormat靠谱多了吧。另外它还内置了非常多的格式化模版实例供以使用,形如:
格式化器 | 示例 |
---|---|
ofLocalizedDate(dateStyle) | ‘2021-01-03’ |
ofLocalizedTime(timeStyle) | ‘10:15:30’ |
ofLocalizedDateTime(dateTimeStyle) | ‘3 Jun 2021 11:05:30’ |
ISO_LOCAL_DATE | ‘2021-12-03’ |
ISO_LOCAL_TIME | ‘10:15:30’ |
ISO_LOCAL_DATE_TIME | ‘2021-12-03T10:15:30’ |
ISO_OFFSET_DATE_TIME | ‘2021-12-03T10:15:30+01:00’ |
ISO_ZONED_DATE_TIME | ‘2021-12-03T10:15:30+01:00[Europe/Paris]’ |
@Test
public void test13() {
System.out.println(DateTimeFormatter.ISO_LOCAL_DATE.format(LocalDate.now()));
System.out.println(DateTimeFormatter.ISO_LOCAL_TIME.format(LocalTime.now()));
System.out.println(DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(LocalDateTime.now()));
}
输出:
2021-01-17
22:43:21.398
2021-01-17T22:43:21.4
复制
若想自定义模式pattern,和Date一样它也可以自己指定任意的pattern 日期/时间模式。由于本文在Date部分详细介绍了日期/时间模式,各个字母代表什么意思以及如何使用,这里就不再赘述了哈。
虽然DateTimeFormatter支持的模式比Date略有增加,但大体还保持一致,个人觉得这块无需再花精力。若真有需要再查官网也不迟
@Test
public void test14() {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("第Q季度 yyyy-MM-dd HH:mm:ss", Locale.US);
// 格式化输出
System.out.println(formatter.format(LocalDateTime.now()));
// 解析
String dateTimeStrParam = "第1季度 2021-01-17 22:51:32";
LocalDateTime localDateTime = LocalDateTime.parse(dateTimeStrParam, formatter);
System.out.println("解析后的结果:" + localDateTime);
}
复制
Q/q:季度,如3; 03; Q3; 3rd quarter。
最佳实践
- 弃用Date,拥抱JSR 310
每每说到JSR 310日期/时间时我都会呼吁,保持惯例我这里继续啰嗦一句:放弃Date甚至禁用Date,使用JSR 310日期/时间吧,它才是日期时间处理的最佳实践。
另外,在使用期间关于制定时区(默认时区时)依旧有一套我心目中的最佳实践存在,这里分享给你:
- 永远显式的指定你需要的时区,即使你要获取的是默认时区
// 方式一:普通做法
LocalDateTime.now();
// 方式二:最佳实践
LocalDateTime.now(ZoneId.systemDefault());
复制
如上代码二者效果一模一样。但是方式二是最佳实践。
理由是:这样做能让代码带有明确的意图,消除模棱两可的可能性,即使获取的是默认时区。拿方式一来说吧,它就存在意图不明确的地方:到底是代码编写者忘记指定时区欠考虑了,还是就想用默认时区呢?这个答案如果不通读上下文是无法确定的,从而造成了不必要的沟通维护成本。因此即使你是要获取默认时区,也请显示的用ZoneId.systemDefault()写上去。
- 使用JVM的默认时区需当心,建议时区和当前会话保持绑定
这个最佳实践在特殊场景用得到。这么做的理由是:JVM的默认时区通过静态方法TimeZone#setDefault()可全局设置,因此JVM的任何一个线程都可以随意更改默认时区。若关于时间处理的代码对时区非常敏感的话,最佳实践是你把时区信息和当前会话绑定,这样就可以不用再受到其它线程潜在影响了,确保了健壮性。
说明:会话可能只是当前请求,也可能是一个Session,具体case具体分析
总结
通过上篇文章 对日期时间相关概念的铺垫,加上本文的实操代码演示,达到弄透Java对日期时间的处理基本不成问题。
两篇文章的内容较多,信息量均比较大,消化起来需要些时间。一方面我建议你先搜藏留以当做参考书备用,另一方面建议多实践,代码这东西只有多写写才能有更深体会。
后面会再用3 -4篇文章对这前面这两篇的细节、使用场景进行补充,比如如何去匹配ZoneId和Offset的对应关系,LocalDateTime、OffsetDateTime、ZonedDateTime跨时区互转问题、在Spring MVC场景下使用的最佳实践等等,敬请关注,一起进步。
全网最全!彻底弄透Java处理GMT/UTC日期时间-腾讯云开发者社区-腾讯云
日期/时间的处理是平时开发中非常常见的场景,若只是简单的格式化场景那就还好,一旦涉及到时区、跨地域跨时区时间转换场景,甚至当还有GMT时间、UTC时间等一堆概念堆上来的时候,总是心理发虚,招架不住。
在地球村的信息化时代背景下,跨国企业/跨国做生意的公司越来越多,所以我们程序员遇到不同时区之间的日期/时间转换/显示的概率大大增加。譬如说:电商平台的商品下单时间,你给中国人页面里展示北京时间是ok的,但你总不能给美国人也展示北京时间吧?否则美国人看到很多订单的下单时间是凌晨1、2点,还以为午夜凶铃呢。
Java在版本8之前用Date类型来表示日期/时间,自版本8起引入了JSR 310日期/时间类型。两套体系对于本地时间、时区时间、带时区的格式化都有着不同的处理办法。
A哥因为跨时区日期转换问题,最近搞了一起生产事故,为此我痛定思痛,决定把经验整理成文,目的是以后再也不踩这方面的坑,同时也帮助大家。
本部分一共会分两篇文章叙述: 1. 概念篇:科普GMT、UTC、时区、时间戳、夏令时等常见概念以及背景 2. 实战篇:在1的基础上(概念必须先知晓,否则实战无法进行),Java是如何来处理GMT/UTC时间、时区、偏移量、夏令时...的
这两篇文章搞完,自己再也不用不担心在日期/时间方面埋bug了。相信我,这两篇文章十分具有收藏价值。
本文提纲
版本约定
- JDK:8
✍正文
下面将围绕一些日期/时间的概念分别做主题讲解,这些名词你无一例外的都听过,但我猜测大概率你并不理解,甚至是知晓它们的区别。
GMT:格林威治时间
格林威治(也称:格林尼治)时间,也叫世界时(Universal Time),也叫世界标准时间。是指位于英国伦敦郊区的【皇家格林尼治天文台】的标准时间,是本初子午线上的地方时,是0时区的区时。
众所周知,中国统一用的北京时间是位于东八区(+8)与标准时间相差8小时。什么含义?举个例子:若GMT(英国伦敦的格林威治)现在是上午11点,那中国北京时间现在就是 11 + 8 = 19点(下午7点)。
将这个公式再抽象一下,可表示为:本地时间=GMT+时区差
北京位于东八区,则时区差N=+8,美国纽约位于西五区,则时区差N=-5。这么算来,若北京时间是晚上23点的话,美国纽约时间就是当天上午10点(23 - 8 - 5 = 10)
凭什么格林威治作为标准时间?
你可能会问,大家都有腰间盘,为何格林威治的那么突出呢?
大背景是这样子的:19世纪开始,世界各国来往开始频繁,而欧洲大陆、美洲大陆和亚洲大陆都有各自的时区,为提高沟通效率避免混乱,各国的代表1884年在美国华盛顿召开了国际大会,选出英国伦敦的格林威治作为全球时间的中心点,并由它负责维护和计算,从1924年开始,格林威治天文台每小时就会向全世界播报时间(截止到1979年)。
在美国华盛顿开会,确定英国伦敦作为时间中心点,还蛮滑稽O(∩_∩)O哈哈~
其实选择英国格林威治最主要原因是:当时大部分的船只都已经以格林威治子午线做为参考标准,毕竟曾经的英国可是日不落帝国,大航海时代末便开始称霸世界,拳头里面出政权。
格林威治天文台在计时领域的权威是非常大的,譬如离我们最近的一次“时间风波”:在即将跨世纪的时候,世界各国对21世纪到底应该从2000年开始还是从2001年开始争论不休,最终还是格林威治天文台出面平息了争论,开新闻发布会宣布21世纪始于2001年。
地球自转
地球绕自转轴自西向东的转动(太阳东起西落),所以东时区的人会比西时区的人早一些看到太阳,从而时间上会早一点。
以本初子午线为中心,按照地球自转方向,每隔经度15°划分一个时区的方法,全球共分为24个时区:东1区至东12区,西1区至西12区,其中东西12区跨度都是7.5°也叫半时区。
中国有哪几个时区?
1个,这是一个错得比较合理的答案。合理是因为中国虽然幅员辽阔,但全国使用统一的北京时间,所以很容易被误以为只有一个时区。
错是因为拍脑袋想一想就知道,中国东西横跨5000+公里,怎么可能只躺在一个时区呢?正确答案是:中国大陆共横跨5个时区,各个时区大致的方位图如下:
假装之类有幅中国地图 假装之类有幅中国地图 假装之类有幅中国地图
看图就清晰明了的知道中国为何选用东八区时间作为全国标准时间了吧?没错,仅就因为北京在东八区,即使地图上只有弹丸大小,但就是这么豪横。
中国用统一时间在沟通上确实方便得多,减少了很多不必要的麻烦。但是也带来一些“小问题”,比如新疆的朋友(位于东5/6区)实际比东八区的北京时间晚了 2-3个小时,我们正常7点天黑准备吃完饭的时候,新疆那边还太阳当空照呢,还蛮有意思的~
美国有哪几个时区?
说到时区,就不得不提及计划再次伟大的美国了。美国同样的幅员辽阔,横跨了4个时区:
如图所示共有四个时区时间,按照图中颜色划分开(并非严格划分,不然出现同一小区隔壁时间比你晚1小时就尴尬了),从右到左依次为: 1. 东部时区(ET):西5区,代表城市:华盛顿特区、纽约、迈阿密等,也称纽约时间。北京时间 = ET + 13h 2. 中部时区(CT):西6区,代表城市:芝加哥、休斯顿等。北京时间 = CT + 14h 3. 山地时区(MT):西7区,代表城市:丹佛、凤凰城等。北京时间 = MT + 15h 4. 太平洋时区(PST):西8区,代表城市:洛杉矶、拉斯维加斯、西雅图等。北京时间 = PST + 16h
GMT和Http协议的渊源
这是我“偶遇”的一个知识点,在这里也一并分享给你。
Http 1.1协议对日期时间传输格式是有严格规定的,支持如下三种格式:
其中第一种格式是互联网传输的标准格式,也是现行的标准。2、3种纯是为了兼容Http 1.0而设计,现在基本已经淘汰没人再会使用,所以事实上的格式只有第一种这1种,作为一个有经验的程序员对这种格式应该不陌生。
另外,还有个关键的知识点:所有HTTP日期/时间戳都必须用格林威治标准时间(GMT)表示,没有例外。对于HTTP来说,GMT完全等于UTC(协调世界时)。
当然喽,这一切都是由浏览器自动帮你完成的,毕竟Http协议是浏览器去搞的不是
UTC:世界标准时间
Coordinated Universal Time直译为:世界协调时间。它是以原子时作为计量单位的时间,计算结果极其严谨和精密。它比GMT时间更来得精准,误差值必须保持在0.9秒以内,倘若大于0.9秒就会通过闰秒来“解决”。
原子时:物质的原子内部发射的电磁振荡频率为基准的时间计量系统。美国的物理实验市在2014年造出了人类历史上最精确的原子钟,50亿年误差1s,可谓相当靠谱了。中国的铯原子钟也能确保2000万年误差不超过1s。
大事记:1979年12月初内瓦举行的世界无线电行政大会通过决议,确定用“世界协调时间(UTC时间)”取代“格林威治时间(GMT时间)”,作为无线电通信领域内的国际标准时间。
UTC和GMT的区别
UTC和GMT都称作世界标准时间,为毛有了GMT还搞出个UTC,到底有何区别,下面做出简述。
GMT:老的时间计量标准,根据地球的自转和公转来计算时间的,自转一圈是一天,公转一圈是一年。但是呢,地球公转的轨道是椭圆形的:
并且后来人们发现地球的自转时间也并不是恒定的,这么一来就会造成有一天时间长一些,有一天时间短一些的情况,误差较大给人感觉时间不那么“精准”了,因此迫切需要一个更加精准的方案来计时,UTC诞生了。
UTC:1967年人类制作出原子钟,从而“发明”了UTC时间正式投入使用。它是真正意义上的标准时间,以原子钟所定义的秒长为基础,UTC时间认为一个太阳日(一天)总是恒定的86400秒(24小时)。
UTC是协调时间,含义为:一切以我为基准,全部想我看齐。所以称它为世界标准时间是没毛病的,而把GMT称作格林威治当地时间更为合适(也叫旧的标准时间)。
UTC和GMT的联系
由于在大多数情况下,UTC时间能与GMT时间互换。对此很多同学就丈二和尚摸不着头脑了,他俩这不就一样的吗?
其实非也。这里用通俗易懂的一句话来告知它俩的联系:UTC是标准时间参照,像GMT(格林威治时间)、ET(美国东部时间)、PST(太平洋时间)、CST(北京时间)等等都是具体的时区时间。GMT能和UTC直接转换,仅仅是因为碰巧GMT是0时区时间,数值上刚好和UTC是相等的(不需要精确到秒的情况下,二者可以视为相等),看起来一样,但是概念含义上请务必区分开来哈。
UTC与偏移量
在日常生活中,我们所使用的时间肯定是本地时间。在只有GMT的时候,本地时间是通过时区计算出来的,而现在UTC才是标准参考,因此采用UTC和偏移量(Offset)的方式来表示本地时间:
这个偏移量可表示为:UTC -
或UTC +
,后面接小时数,分钟数
。如:UTC +9:30表示澳大利亚中央标准时间,UTC +8表示中国标准时间。偏移量常见的表示形式有:±[hh]:[mm]
、±[hh][mm]
、±[hh]
这三种方式均可。
举个例子:现在UTC时间是10:30z
(z表示偏移量=0),那么北京时间现在若是1630 +0800
(下午4点半),对应的纽约时间就是0530 -0500
(早上5点半)。
注意:在UTC的世界里并无时区的概念,而是偏移量(时间点跟上偏移量才是一个正规的UTC时间),它和时区并无直接关系
可以看到偏移量可以精确到分钟级别控制,非常精细化。全球只有24个时区(只能精确到小时),但偏移量有“无数个”。当然喽为了方便沟通,时间日期联盟组织把世界主要国家/城市的偏移量汇总起来且都给取了个Time zone name
名称用于沟通,共好几百个,部分截图如下:
偏移量和国家/城市名称的全部对应关系,请参考网址(直接访问,无需梯子):https://www.timeanddate.com/time/zones
CST
CST这个缩写比较尴尬的是它可以同时代表四个不同的时间: - CST (China Standard Time) :中国标准时间 UTC+8:00 - Central Standard Time (USA) UTC-6:00 - Central Standard Time (Australia) UTC+9:30 - Cuba Standard Time UTC-4:00
CST到底啥意思就看如何翻译喽,所以需要根据上下文语境自行抉择哈。
ISO
在时间日期上它全称是ISO 8601,是一种日期/时间表示方法的规范。规定了一种明确的、国际上都能理解的日历和时钟格式。
这一ISO标准有助于消除各种日-日惯例、文化和时区对全球业务产生的影响。它提供了一种显示日期和时间的方式,这种方式是明确定义的,对人和机器都是可以理解的。当日期用数字表示时,它们可以以不同的方式进行解释。例如,01/05/12可以表示2012年1月5日或2012年5月1日。在个人层面上,这种不确定性可能非常令人沮丧,在商业环境中,它可能非常昂贵。在日期不明确的情况下,组织会议和交付、书写合同和购买机票都是非常困难的。
ISO 8601通过制定一种国际公认的日期表示方式来解决这种不确定性:YYYY-MM-DD
。例如 September 27, 2012就会被表示为2012-09-27。
很多开发语言内置了一些常用的ISO标准日期/时间格式,如Java中的: - ISO.DATE:yyyy-MM-dd, e.g. "2000-10-31" - ISO.TIME:HH:mm:ss.SSSXXX, e.g. "01:30:00.000-05:00" - ISO.DATE_TIME:yyyy-MM-dd'T'HH:mm:ss.SSSXXX, e.g. "2000-10-31T01:30:00.000-05:00".
夏令时
DST(Daylight Saving Time),夏令时又称夏季时间(可没有冬令时哦)。它是为节约能源而人为规定地方时间的制度(鼓励人们早睡早起,不要浪费电,夏天日照时间长尽量多用自然资源),全球约40%的国家在夏季使用夏令时,其他国家则全年只使用标准时间。正在使用夏令时的代表国家:美国、欧盟、俄罗斯等等。
每年的夏令时时间段还不一样(一般在3月的第2个周日开始),比如美国2020年夏令时时间是:2020年3月8日 - 2020年11月1日。具体做法是:在3.8号这天将时钟往前拨拨1个小时,11.1号这天还原回来。
中国在1986 - 1992年短暂搞过一段时间,但太麻烦就“废弃”了
大事记:目前全世界有近110个国家每年要实行夏令时。 自2011年3月27日开始俄罗斯永久使用夏令时,把时间拨快一小时,不再调回。
时间戳
现实生活的世界里,时间是不断向前的,如果向前追溯时间的起点,可能是宇宙出生时,又或是是宇宙出现之前,但肯定是我们目前无法找到的,我们不知道现在距离时间原点的精确距离。所以我们要表示时间, 就需要人为定义一个原点。它就是:格林威治时间(GMT)1970年1月1日的午夜0点0分0秒。
时间戳一般指的UNIX时间,或类UNIX系统(比如Linux、MacOS等)使用的时间表示方式。定义为:从UTC时间的1970-1-1 0:0:0
起到现在的总秒数(秒是毫秒、微妙、纳秒的总称)。
但是不可忽略的一个case:由于闰秒的存在,那么当闰秒发生时,就极有可能出现同一个时间戳表示两个时刻的情况(类似时钟回拨),而且闰秒还没规律所以无法程序式的避免,怎么破?
这个时候就需要一种专门的对时协议来保证了,它就是:网络时间协议。
网络时间协议
网络时间协议 Network Time Protocol(NTP)是用来使计算机时间同步化的一种协议,它可以使计算机对其服务器或时钟源(如石英钟,GPS等等)做同步化,它可以提供高精准度的时间校正(LAN上与标准间差小于1毫秒,WAN上几十毫秒),且可介由加密确认的方式来防止恶毒的协议攻击。
NTP的目的是在无序的Internet环境中提供精确和健壮的时间服务,各大操作系统(windows/Linux)对NTP都有实现。
✍总结
简单地讲呢,GMT格林威治时间可认为是以前的标准时间,而UTC时间是现在在使用的世界时间标准;时区是以本初子午线为中心来划分的,东为正西为负,本处子午线就位于英国伦敦的格林威治;夏令时是地方的时间制度(并非全球标准),施行夏令时的地方,每年有2天很特殊,即一天只有23个小时,另一天则有25个小时。
从源头上彻底了解了这些概念,将会让我们在处理与时间相关的问题时如虎添翼。本文介绍了好些个日期/时间方面的概念,文字偏多,所以建议你收藏起来当作参考书来使用。
下篇文章将会接着本文内容,站在实战的角度,介绍Java是如何实现GMT和UTC时间的,以及各种case下的使用和避坑指南,欢迎关注我。