zl程序教程

您现在的位置是:首页 >  后端

当前栏目

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

JAVA日期 处理 时间 最全 彻底 全网 utc
2023-09-27 14:25:55 时间
ZoneId


image.png

它代表一个时区的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 如图


image.png


image.png


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


这里第一个参数传的前缀 可用值为 “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引起你的重视 对此结果我做如下解释


image.png


如图 在2021.03.14 - 2021.11.07期间 纽约的偏移量是-4 其余时候是-5。本例的日期是2021-05-05处在夏令时之中 因此偏移量是-4 这就解释了为何你显示的写了-5最终还是成了-4。


JSR 310格式化

针对JSR 310日期时间类型的格式化/解析 有个专门的类java.time.format.DateTimeFormatter用于处理。


DateTimeFormatter也是一个不可变的类 所以是线程安全的 比SimpleDateFormat靠谱多了吧。另外它还内置了非常多的格式化模版实例供以使用 形如


image.png


 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场景下使用的最佳实践等等 敬请关注 一起进步。


本文思考题


看完了不一定懂 看懂了不一定会。来 文末3个思考题帮你复盘


Date类型如何处理夏令时 ZoneId和ZoneOffset有什么区别 平时项目若遇到日期时间的处理 有哪些最佳实践



java时间换算(BJU转UTC) UTC是世界协调时,BJT是北京时间,UTC时间相当于BJT减去8。现在,你的程序要读入一个整数,表示BJT的时和分。整数的个位和十位表示分,百位和千位表示小时。如果小时小于10,则没有千位部分;如果小时是0,则没有百位部分;如果分小于10分,需要保留十位上的0。如1124表示11点24分,而905表示9点5分,36表示0点36分,7表示0点7分。