全网最全!彻底弄透Java处理GMT/UTC日期时间(上)

简介: 全网最全!彻底弄透Java处理GMT/UTC日期时间(上)

前言


你好,我是A哥(YourBatman)。


本系列的目的是明明白白、彻彻底底的搞定日期/时间处理的几乎所有case。上篇文章 铺设所有涉及到的概念解释,例如GMT、UTC、夏令时、时间戳等等,若你还没看过,不仅强烈建议而是强制建议你前往用花5分钟看一下,因为日期时间处理较为特殊,实战必须基于对概念的了解,否则很可能依旧雾里看花。


说明:日期/时间的处理是日常开发非常常见的老大难,究其原因就是对日期时间的相关概念、应用场景不熟悉,所以不要忽视它


上篇概念,本文落地实操,二者相辅相成,缺一不可。本文内容较多,文字较长,预计超2w字,旨在全面的彻底帮你搞定Java对日期时间的处理,建议你可收藏,作为参考书留以备用。


本文提纲


image.png



版本约定

  • JDK:8


正文


上文铺了这么多概念,作为一枚Javaer最关心当然是这些“概念”在Java里的落地。平时工作中遇到时间如何处理?用Date还是JDK 8之后的日期时间API?如何解决跨时区转换等等头大问题。A哥向来管生管养,管杀管埋,因此本文就带你领略一下,Java是如何实现GMT和UTC的?


众所周知,JDK以版本8为界,有两套处理日期/时间的API:


image.png


虽然我一直鼓励弃用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让我们有多种方式可以手动设置/修改默认时区:


  1. API方式: 强制将时区设为北京时区TimeZone.setDefault(TimeZone.getDefault().getTimeZone("GMT+8"));
  2. JVM参数方式:-Duser.timezone=GMT+8
  3. 运维设置方式:将操作系统主机时区设置为北京时区,这是推荐方式,可以完全对开发者无感,也方便了运维统一管理


据我了解,很多公司在阿里云、腾讯云、国内外的云主机上部署应用时,全部都是采用运维设置统一时区:中国时区,这种方式来管理的,这样对程序来说就消除了默认时区不一致的问题,对开发者友好。


让人恼火的夏令时


你知道吗,中国曾经也使用过夏令时。


什么是夏令时?戳这里


离现在最近是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设置时区(那就用默认时区呗)就可正常转换。但是作为高手的你必须清清楚楚,明明白白的知道这是由于交互双发默认有个相同时区的契约存在。

相关文章
|
2月前
|
Java
Java基础之日期和时间
Java基础之日期和时间
22 1
|
3月前
|
安全 Java 程序员
Java8实战-新的日期和时间API
Java8实战-新的日期和时间API
35 3
|
4月前
|
安全 Java Unix
Java语言中的日期与时间处理技术
Java语言中的日期与时间处理技术
|
1月前
|
前端开发 JavaScript Java
【前端学java】java中的日期操作(13)
【8月更文挑战第10天】java中的日期操作
16 2
【前端学java】java中的日期操作(13)
|
29天前
|
Java
比较两个日期是否相等Java
这篇文章提供了Java中比较两个日期是否相等的两种方法:使用`Calendar`类和`SimpleDateFormat`类来确定两个日期是否为同一天,并附有详细的代码示例和测试结果。
|
2月前
|
Java API
Java基础之日期和时间
【7月更文挑战第4天】 Java日期时间API概览:Java 8之前,Date表示不可变时间点,Calendar用于计算,SimpleDateFormat处理格式化。Date的setXXX方法不推荐,Calendar支持加减操作。时区处理用TimeZone,Time仅处理时间(不含日期)。Java 8引入java.time包,提供Instant、LocalDate等线程安全类,改进了性能和易用性,支持时区和更复杂操作。后续将探讨Java 8的新特性!
29 1
|
2月前
|
安全 Java API
Java基础之日期和时间
【7月更文挑战第3天】 Java 8之前的日期时间处理涉及Date、Calendar、SimpleDateFormat及TimeZone类。Date是不可变对象,但不推荐直接调整时间。Calendar提供日期计算,而SimpleDateFormat用于格式化。Date和Calendar非线程安全,处理时区需用TimeZone。Java 8引入了java.time包,包含Instant、LocalDate等类,提供更现代、线程安全和高效的API,例如Instant用于时间戳,LocalDateTime表示日期和时间,ZonedDateTime处理时区。下节将探讨Java 8的新API。
91 2
|
2月前
|
Java API
Java中的日期和时间API详解
Java中的日期和时间API详解
|
3月前
|
Java 测试技术 API
滚雪球学Java(52):一步一步教你使用Java Calendar类进行日期计算
【6月更文挑战第6天】🏆本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
30 3
滚雪球学Java(52):一步一步教你使用Java Calendar类进行日期计算
|
3月前
|
Java
2021蓝桥杯大赛软件类国赛Java大学B组 完全日期 复杂遍历搜索
2021蓝桥杯大赛软件类国赛Java大学B组 完全日期 复杂遍历搜索
39 2