写点什么

一篇文章!彻底弄透 Java 处理 GMT-UTC 日期时间,java 百度天气接口 api

用户头像
极客good
关注
发布于: 刚刚

也就是说,同一个毫秒值,根据时区/偏移量的不同可以展示多地的时间,这就证明了 Date 它的时区无关性。


确切地说:Date 对象里存的是自格林威治时间( GMT)1970 年 1 月 1 日 0 点至 Date 所表示时刻所经过的毫秒数,是个数值。

读取字符串为 Date 类型

这是开发中极其常见的一种需求:client 请求方扔给你一个字符串如"2021-01-15 18:00:00",然后你需要把它转为 Date 类型,怎么破?


问题来了,光秃秃的扔给我个字符串说是 15 号晚上 6 点时间,我咋知道你指的是北京的晚上 6 点,还是东京的晚上 6 点呢?还是纽约的晚上 6 点呢?



因此,对于字符串形式的日期时间,只有指定了时区才有意义。也就是说字符串 + 时区 才能精确知道它是什么时刻,否则是存在歧义的。


也许你可能会说了,自己平时开发中前端就是扔个字符串给我,然后我就给格式化为一个 Date 类型,并没有传入时区参数,运行这么久也没见出什么问题呀。如下所示:


@Testpublic 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/ShanghaiWed Jan 15 18:00:00 CST 2020


看起来结果没问题。事实上,这是因为默认情况下你们交互双发就达成了契约:双方均使用的是北京时间(时区),既然是相同时区,所以互通有无不会有任何问题。不信你把你接口给海外用户调试试?


对于格式化器来讲,虽然说编程过程中一般情况下我们并不需要给 DateFormat 设置时区(那就用默认时区呗)就可正常转换。但是作为高手的你必须清清楚楚,明明白白地知道这是由于交互双发默认有个相同时区的契约存在

SimpleDateFormat 格式化

Java 中对 Date 类型的输入输出/格式化,推荐使用 DateFormat 而非用其toString()方法。


DateFormat 是一个时间格式化器抽象类,SimpleDateFormat 是其具体实现类,用于以语言环境敏感的方式格式化和解析日期。它允许格式化(日期→文本)、解析(文本→日期)和规范化。


划重点:对语言环境敏感,也就是说对环境 Locale、时区 TimeZone 都是敏感的。既然敏感,那就是可定制的


对于一个格式化器来讲,模式(模版)是其关键因素,了解一下:


日期/时间模式:格式化的模式由指定的字符串组成,未加引号的大写/小写字母(A-Z a-z)代表特定模式,用来表示模式含义,若想原样输出可以用单引号''包起来,除了英文字母其它均不解释原样输出/匹配。下面是它规定的模式字母(其它字母原样输出):



这个表格里出现了一些“特殊”的匹配类型,做如下解释:


  • Text:格式化(Date -> String),如果模式字母的数目是 4 个或更多,则使用完整形式;否则,如果可能的话,使用简短或缩写形式。对于解析(String -> Date),这两种形式都一样,与模式字母的数量无关


@Testpublic 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 解析时总结出两点结论:


  1. 英文单词,不分区大小写。如 SatUrday sunDay 都是没问题,但是不能有拼写错误

  2. 若有多个 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 的使用再强调这两点哈:


  1. SimpleDateFormat 并非线程安全类,使用时请务必注意并发安全问题

  2. 若使用 SimpleDateFormat 去格式化成非本地区域(默认 Locale)的话,那就必须在构造的时候就指定好,如 Locale.US

  3. 对于 Date 类型的任何格式化、解析请统一使用 SimpleDateFormat

JSR 310 类型

曾经有个人做了个很有意思的投票,统计对 Java API 的不满意程度。最终 Java Date/Calendar API 斩获第二烂(第一烂是 Java XML/DOM),体现出它烂的点较多,这里给你例举几项:


  1. 定义并不一致,在 java.util 和 java.sql 包中竟然都有 Date 类,而且呢对它进行格式化/解析类竟然又跑到 java.text 去了,精神分裂啊

  2. java.util.Date 等类在建模日期的设计上行为不一致,缺陷明显。包括易变性、糟糕的偏移值、默认值、命名等等

  3. java.util.Date 同时包含日期和时间,而其子类 java.sql.Date 却仅包含日期,这是什么神继承?



@Testpublic 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 2021java.sql.Date:2021-01-16java.sql.Time:21:50:36java.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 的偏移量。


值得提前强调,时区和偏移量在概念和实际作用上是有较大区别的,主要体现在:


  1. UTC 偏移量仅仅记录了偏移的小时分钟而已,除此之外无任何其它信息。举个例子:+08:00 的意思是比 UTC 时间早 8 小时,没有地理/时区含义,相应的-03:30 代表的意思仅仅是比 UTC 时间晚 3 个半小时

  2. 时区是特定于地区而言的,它和地理上的地区(包括规则)强绑定在一起。比如整个中国都叫东八区,纽约在西五区等等


中国没有夏令时,所有东八区对应的偏移量永远是+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:


  1. 最简单的 ID 类型:ZoneOffset,它由'Z'和以'+'或'-'开头的 id 组成。如:Z、+18:00、-18:00

  2. 另一种类型的 ID 是带有某种前缀形式的偏移样式 ID,例如'GMT+2'或'UTC+01:00'。可识别的(合法的)前缀是'UTC', 'GMT'和'UT'

  3. 第三种类型是基于区域的 ID(推荐使用)。基于区域的 ID 必须包含两个或多个字符,且不能以'UTC'、'GMT'、'UT' '+'或'-'开头。基于区域的 id 由配置定义好的,如 Europe/Paris


概念说了一大堆,下面给几个代码示例感受下吧。


1、获取系统默认的 ZoneId:


@Testpublic void test1() {// JDK 1.8 之前做法 System.out.println(TimeZone.getDefault());// JDK 1.8 之后做法 System.out.println(ZoneId.systemDefault());}


输出:Asia/Shanghaisun.util.calendar.ZoneInfo[id="Asia/Shanghai",offset=28800000,dstSavings=0,useDaylight=false,transitions=29,lastRule=null]


二者结果是一样的,都是 Asia/Shanghai。因为 ZoneId 方法底层就是依赖 TimeZone,如图:




2、指定字符串得到一个 ZoneId:


@Testpublic void test2() {System.out.println(ZoneId.of("Asia/Shanghai"));// 报错:java.time.zone.ZoneRulesException: Unknown time-zone ID: Asia/xxxSystem.out.println(ZoneId.of("Asia/xxx"));}


很明显,这个字符串也是不能随便写的。那么问题来了,可写的有哪些呢?同样的 ZoneId 提供了 API 供你获取到所有可用的字符串 id,有兴趣的同学建议自行尝试:


@Testpublic void test3() {ZoneId.getAvailableZoneIds();}


3、根据偏移量得到一个 ZoneId:


@Testpublic void test4() {ZoneId zoneId = ZoneId.ofOffset("UTC", ZoneOffset.of("+8"));System.out.println(zoneId);// 必须是大写的 ZzoneId = ZoneId.ofOffset("UTC", ZoneOffset.of("Z"));System.out.println(zoneId);}


输出:UTC+08:00UTC


这里第一个参数传的前缀,可用值为:"GMT", "UTC", or "UT"。当然还可以传空串,那就直接返回第二个参数 ZoneOffset。若以上都不是就报错


注意:根据偏移量得到的 ZoneId 内部并无现成时区规则可用,因此对于有夏令营的国家转换可能出问题,一般不建议这么去做。


4、从日期里面获得时区:


@Testpublic 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、最小/最大偏移量:因为偏移量传入的是数字,这个是有限制的哦


@Testpublic 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、通过时分秒构造偏移量(使用很方便,推荐):


@Testpublic 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 表示是一个带有时区的日期时间,它们能非常方便的互相完成转换。


@Testpublic void test8() {// 本地日期/时间 System.out.println("================本地时间================");System.out.println(LocalDate.now());System.out.println(L


【一线大厂Java面试题解析+核心总结学习笔记+最新架构讲解视频+实战项目源码讲义】
浏览器打开:qq.cn.hn/FTf 免费领取
复制代码


ocalTime.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-1709:18:40.7032021-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:002021-01-16T20:18:40.710-05:002021-01-17T01:18:40.710Z


本地时间的输出非常“干净”,可直接用于显示。带时区的时间显示了该时间代表的是哪个时区的时间,毕竟不指定时区的时间是没有任何意义的。LocalXXX 因为它具有时区无关性,因此它不能代表一个瞬间/时刻。


另外,关于 LocalDateTime、OffsetDateTime、ZonedDateTime 三者的跨时区转换问题,以及它们的详解,因为内容过多放在了下文专文阐述,保持关注。

读取字符串为 JSR 310 类型

一个独立的日期时间类型字符串如 2021-05-05T18:00-04:00 它是没有任何意义的,因为没有时区无法确定它代表那个瞬间,这是理论当然也适合 JSR 310 类型喽。


遇到一个日期时间格式字符串,要解析它一般有这两种情况:


  1. 不带时区/偏移量的字符串:要么不理它说转换不了,要么就约定一个时区(一般用系统默认时区),使用 LocalDateTime 来解析


@Testpublic void test11() {String dateTimeStrParam = "2021-05-05T18:00";LocalDateTime localDateTime = LocalDateTime.parse(dateTimeStrParam);System.out.println("解析后:" + localDateTime);}


输出:解析后:2021-05-05T18:00


  1. 带时区字/偏移量的符串:


@Testpublic void test12() {// 带偏移量 使用 OffsetDateTimeString dateTimeStrParam = "2021-05-05T18:00-04:00";OffsetDateTime offsetDateTime = OffsetDateTime.parse(dateTimeStrParam);System.out.println("带偏移量解析后:" + offsetDateTime);


// 带时区 使用 ZonedDateTimedateTimeStrParam = "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 靠谱多了吧。另外它还内置了非常多的格式化模版实例供以使用,形如:



@Testpublic 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()));

用户头像

极客good

关注

还未添加个人签名 2021.03.18 加入

还未添加个人简介

评论

发布
暂无评论
一篇文章!彻底弄透Java处理GMT-UTC日期时间,java百度天气接口api