你好,我是A哥(YourBatman)。

在JSR 310日期工夫体系了,一共有三个API可用于示意日期工夫:

  • LocalDateTime:本地日期工夫
  • OffsetDateTime:带偏移量的日期工夫
  • ZonedDateTime:带时区的日期工夫

兴许平时开发中你只用到过LocalDateTime这个API,那是极好的,然而不能止步于此,否则就图样图森破了。

随着场景的多样性变动,咱们开发者接触到OffsetDateTime/ZonedDateTime的概率越来越大,凡是和国际化产生上关系的大概率都会用失去它们。本文仍然站在实用的角度,辅以具体代码示例,介绍它三。

本文提纲

版本约定

  • JDK:8

注释

上面这张图是一个残缺的日期工夫,拆解各个局部的含意,高深莫测(倡议珍藏此图):

因为LocalDate、LocalTime等了解起来比较简单,就不必再花笔墨介绍了,重点放在LocalDateTime、OffsetDateTime、ZonedDateTime它三身上。

什么是LocalDateTime?

ISO-8601日历零碎中不带时区的日期工夫。

阐明:ISO-8601日零碎是现今世界上绝大部分国家/地区应用的,这就是咱们国人所说的公历,有平年的个性

LocalDateTime是一个不可变的日期-工夫对象,它示意一个日期工夫,通常被视为年-月-日-小时-分钟-秒。还能够拜访其余日期和工夫字段,如day-of-year、day-of-week和week-of-year等等,它的精度能达纳秒级别。

该类不存储时区,所以适宜日期的形容,比方用于生日、deadline等等。然而请记住,如果没有偏移量/时区等附加信息,一个工夫是不能示意工夫线上的某一时刻的。

代码示例

最大/最小值:

@Testpublic void test1() {    LocalDateTime min = LocalDateTime.MIN;    LocalDateTime max = LocalDateTime.MAX;    System.out.println("LocalDateTime最小值:" + min);    System.out.println("LocalDateTime最大值:" + max);    System.out.println(min.getYear() + "-" + min.getMonthValue() + "-" + min.getDayOfMonth());    System.out.println(max.getYear() + "-" + max.getMonthValue() + "-" + max.getDayOfMonth());}输入:LocalDateTime最小值:-999999999-01-01T00:00LocalDateTime最大值:+999999999-12-31T23:59:59.999999999-999999999-1-1999999999-12-31

结构:

@Testpublic void test2() {    System.out.println("以后时区的本地工夫:" + LocalDateTime.now());    System.out.println("以后时区的本地工夫:" + LocalDateTime.of(LocalDate.now(), LocalTime.now()));    System.out.println("纽约时区的本地工夫:" + LocalDateTime.now(ZoneId.of("America/New_York")));}输入:以后时区的本地工夫:2021-01-17T17:00:41.446以后时区的本地工夫:2021-01-17T17:00:41.447纽约时区的本地工夫:2021-01-17T04:00:41.450

留神,最初一个结构传入了ZoneId,并不是说LocalDateTime和时区无关了,而是通知说这个Local指的是纽约,细品这句话。

计算:

@Testpublic void test3() {    LocalDateTime now = LocalDateTime.now(ZoneId.systemDefault());    System.out.println("计算前:" + now);    // 加3天    LocalDateTime after = now.plusDays(3);    // 减4个小时    after = after.plusHours(-3); // 成果同now.minusDays(3);    System.out.println("计算后:" + after);    // 计算时间差    Period period = Period.between(now.toLocalDate(), after.toLocalDate());    System.out.println("相差天数:" + period.getDays());    Duration duration = Duration.between(now.toLocalTime(), after.toLocalTime());    System.out.println("相差小时数:" + duration.toHours());}输入:计算前:2021-01-17T17:10:15.381计算后:2021-01-20T14:10:15.381相差天数:3相差小时数:-3

格式化:

@Testpublic void test4() {    LocalDateTime now = LocalDateTime.now(ZoneId.systemDefault());    // System.out.println("格式化输入:" + DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(now));    System.out.println("格式化输入(本地化输入,中文环境):" + DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT, FormatStyle.SHORT).format(now));    String dateTimeStrParam = "2021-01-17 18:00:00";    System.out.println("解析后输入:" + LocalDateTime.parse(dateTimeStrParam, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss", Locale.US)));}输入:格式化输入(本地化输入,中文环境):21-1-17 下午5:15解析后输入:2021-01-17T18:00

什么是OffsetDateTime?

ISO-8601日历零碎中与UTC偏移量无关的日期工夫。OffsetDateTime是一个带有偏移量的日期工夫类型。存储有准确到纳秒的日期工夫,以及偏移量。能够简略了解为 OffsetDateTime = LocalDateTime + ZoneOffset。

OffsetDateTime、ZonedDateTime和Instant它们三都能在工夫线上以纳秒精度存储一个霎时(请留神:LocalDateTime是不行的),也可了解我某个时刻。OffsetDateTime和Instant可用于模型的字段类型,因为它们都示意霎时值并且还不可变,所以适宜网络传输或者数据库长久化。

ZonedDateTime不适宜网络传输/长久化,因为即便同一个ZoneId时区,不同中央获取到瞬时值也有可能不一样

代码示例

最大/最小值:

@Testpublic void test5() {    OffsetDateTime min = OffsetDateTime.MIN;    OffsetDateTime max = OffsetDateTime.MAX;    System.out.println("OffsetDateTime最小值:" + min);    System.out.println("OffsetDateTime最大值:" + max);    System.out.println(min.getOffset() + ":" + min.getYear() + "-" + min.getMonthValue() + "-" + min.getDayOfMonth());    System.out.println(max.getOffset() + ":" + max.getYear() + "-" + max.getMonthValue() + "-" + max.getDayOfMonth());}输入:OffsetDateTime最小值:-999999999-01-01T00:00+18:00OffsetDateTime最大值:+999999999-12-31T23:59:59.999999999-18:00+18:00:-999999999-1-1-18:00:999999999-12-31

偏移量的最大值是+18,最小值是-18,这是由ZoneOffset外部的限度决定的。

结构:

@Testpublic void test6() {    System.out.println("以后地位偏移量的本地工夫:" + OffsetDateTime.now());    System.out.println("偏移量-4(纽约)的本地工夫::" + OffsetDateTime.of(LocalDateTime.now(), ZoneOffset.of("-4")));    System.out.println("纽约时区的本地工夫:" + OffsetDateTime.now(ZoneId.of("America/New_York")));}输入:以后地位偏移量的本地工夫:2021-01-17T19:02:06.328+08:00偏移量-4(纽约)的本地工夫::2021-01-17T19:02:06.329-04:00纽约时区的本地工夫:2021-01-17T06:02:06.330-05:00

计算:

格式化:

@Testpublic void test7() {    OffsetDateTime now = OffsetDateTime.now(ZoneId.systemDefault());    System.out.println("格式化输入(本地化输入,中文环境):" + DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT, FormatStyle.SHORT).format(now));    String dateTimeStrParam = "2021-01-17T18:00:00+07:00";    System.out.println("解析后输入:" + OffsetDateTime.parse(dateTimeStrParam));}输入:格式化输入(本地化输入,中文环境):21-1-17 下午7:06解析后输入:2021-01-17T18:00+07:00

转换:
LocalDateTime -> OffsetDateTime

@Testpublic void test8() {    LocalDateTime localDateTime = LocalDateTime.of(2021, 01, 17, 18, 00, 00);    System.out.println("以后时区(北京)工夫为:" + localDateTime);    // 转换为偏移量为 -4的OffsetDateTime工夫    // 1、-4中央的早晨18点    System.out.println("-4偏移量中央的早晨18点:" + OffsetDateTime.of(localDateTime, ZoneOffset.ofHours(-4)));    System.out.println("-4偏移量中央的早晨18点(形式二):" + localDateTime.atOffset(ZoneOffset.ofHours(-4)));    // 2、北京工夫早晨18:00 对应的-4中央的工夫点    System.out.println("以后地区对应的-4中央的工夫:" + OffsetDateTime.ofInstant(localDateTime.toInstant(ZoneOffset.ofHours(8)), ZoneOffset.ofHours(-4)));}输入:以后时区(北京)工夫为:2021-01-17T18:00-4偏移量中央的早晨18点:2021-01-17T18:00-04:00-4偏移量中央的早晨18点(形式二):2021-01-17T18:00-04:00以后地区对应的-4中央的工夫:2021-01-17T06:00-04:00

通过此例值得注意的是:LocalDateTime#atOffset()/atZone()只是减少了偏移量/时区,本地工夫是并没有扭转的。若想实现本地工夫到其它偏移量的对应的工夫只能通过其ofInstant()系列构造方法。

OffsetDateTime -> LocalDateTime

@Testpublic void test81() {    OffsetDateTime offsetDateTime = OffsetDateTime.of(LocalDateTime.now(), ZoneOffset.ofHours(-4));    System.out.println("-4偏移量工夫为:" + offsetDateTime);    // 转为LocalDateTime 留神:工夫还是未变的哦    System.out.println("LocalDateTime的示意模式:" + offsetDateTime.toLocalDateTime());}输入:-4偏移量工夫为:2021-01-17T19:33:28.139-04:00LocalDateTime的示意模式:2021-01-17T19:33:28.139

什么是ZonedDateTime?

ISO-8601国际标准日历零碎中带有时区的日期工夫。它存储所有的日期和工夫字段,精度为纳秒,以及一个时区,带有用于解决不明确的本地日期工夫的时区偏移量。

这个API能够解决从LocalDateTime -> Instant -> ZonedDateTime的转换,其中用zone时区来示意偏移量(并非间接用offset哦)。两个工夫点之间的转换会波及到应用从ZoneId拜访的规定计算偏移量(换句话说:偏移量并非写死而是依据规定计算出来的)。

获取霎时的偏移量很简略,因为每个霎时只有一个无效的偏移量。然而,获取本地日期工夫的偏移量并不简略。存在这三种状况:

  • 失常状况:有一个无效的偏移量。对于一年中的绝大多数工夫,实用失常状况,即本地日期工夫只有一个无效的偏移量
  • 工夫间隙状况:没有无效偏移量。这是因为夏令时开始时从“夏季”改为“冬季”而导致时钟向前拨的时候。在间隙中,没有无效偏移量
  • 重叠状况:有两个无效偏移量。这是因为秋季夏令时从“冬季”到“夏季”的变动,时钟会向后拨。在重叠局部中,有两个无效偏移量

这三种状况如果要本人解决,预计头都大了。这就是应用JSR 310的劣势,ZonedDateTime全帮你搞定,让你应用无忧。

ZonedDateTime可简略认为是LocalDateTime和ZoneId的组合。而ZoneOffset是其内置的动静计算出来的一个主要信息,以确保输入一个瞬时值而存在,毕竟在某个霎时偏移量ZoneOffset必定是确定的。ZonedDateTime也能够了解为保留的状态相当于三个独立的对象:LocalDateTime、ZoneId和ZoneOffset。某个霎时 = LocalDateTime + ZoneOffset。ZoneId确定了偏移量如何扭转的规定。所以偏移量咱们并不能自在设置(不提供set办法,结构时也不行),因为它由ZoneId来管制的。

代码示例

结构:

@Testpublic void test9() {    System.out.println("以后地位偏移量的本地工夫:" + ZonedDateTime.now());    System.out.println("纽约时区的本地工夫:" + ZonedDateTime.of(LocalDateTime.now(), ZoneId.of("America/New_York")));    System.out.println("北京实现对应的纽约时区的本地工夫:" + ZonedDateTime.now(ZoneId.of("America/New_York")));}输入:以后地位偏移量的本地工夫:2021-01-17T19:25:10.520+08:00[Asia/Shanghai]纽约时区的本地工夫:2021-01-17T19:25:10.521-05:00[America/New_York]北京实现对应的纽约时区的本地工夫:2021-01-17T06:25:10.528-05:00[America/New_York]

计算:

格式化:

转换:
LocalDateTime -> ZonedDateTime

@Testpublic void test10() {    LocalDateTime localDateTime = LocalDateTime.of(2021, 01, 17, 18, 00, 00);    System.out.println("以后时区(北京)工夫为:" + localDateTime);    // 转换为偏移量为 -4的OffsetDateTime工夫    // 1、-4中央的早晨18点    System.out.println("纽约时区早晨18点:" + ZonedDateTime.of(localDateTime, ZoneId.of("America/New_York")));    System.out.println("纽约时区早晨18点(形式二):" + localDateTime.atZone(ZoneId.of("America/New_York")));    // 2、北京工夫早晨18:00 对应的-4中央的工夫点    System.out.println("北京地区此工夫对应的纽约的工夫:" + ZonedDateTime.ofInstant(localDateTime.toInstant(ZoneOffset.ofHours(8)), ZoneOffset.ofHours(-4)));    System.out.println("北京地区此工夫对应的纽约的工夫:" + ZonedDateTime.ofInstant(localDateTime, ZoneOffset.ofHours(8), ZoneOffset.ofHours(-4)));}输入:以后时区(北京)工夫为:2021-01-17T18:00纽约时区早晨18点:2021-01-17T18:00-05:00[America/New_York]纽约时区早晨18点(形式二):2021-01-17T18:00-05:00[America/New_York]北京地区此工夫对应的纽约的工夫:2021-01-17T06:00-04:00北京地区此工夫对应的纽约的工夫:2021-01-17T06:00-04:00

OffsetDateTime -> ZonedDateTime

@Testpublic void test101() {    OffsetDateTime offsetDateTime = OffsetDateTime.of(LocalDateTime.now(), ZoneOffset.ofHours(-4));    System.out.println("-4偏移量工夫为:" + offsetDateTime);    // 转换为ZonedDateTime的示意模式    System.out.println("ZonedDateTime的示意模式:" + offsetDateTime.toZonedDateTime());    System.out.println("ZonedDateTime的示意模式:" + offsetDateTime.atZoneSameInstant(ZoneId.of("America/New_York")));    System.out.println("ZonedDateTime的示意模式:" + offsetDateTime.atZoneSimilarLocal(ZoneId.of("America/New_York")));}-4偏移量工夫为:2021-01-17T19:43:28.320-04:00ZonedDateTime的示意模式:2021-01-17T19:43:28.320-04:00ZonedDateTime的示意模式:2021-01-17T18:43:28.320-05:00[America/New_York]ZonedDateTime的示意模式:2021-01-17T19:43:28.320-05:00[America/New_York]

本例有值得关注的点:

  • atZoneSameInstant():将此日期工夫与时区联合起来创立ZonedDateTime,以确保后果具备雷同的Instant

    • 所有偏移量-4 -> -5,工夫点也从19 -> 18,确保了Instant保持一致嘛
  • atZoneSimilarLocal:将此日期工夫与时区联合起来创立ZonedDateTime,以确保后果具备雷同的本地工夫

    • 所以间接成果和toLocalDateTime()是一样的,然而它会尽可能的保留偏移量(所以你看-4变为了-5,放弃了实在的偏移量)

我这里贴出纽约2021年的夏令时工夫区间:

也就是说在2021.03.14 - 2021.11.07期间,纽约的偏移量是-4,其余时候是-5。那么再看这个例子(我把工夫改为5月5号,也就是处于夏令营期间):

@Testpublic void test101() {    OffsetDateTime offsetDateTime = OffsetDateTime.of(LocalDateTime.of(2021, 05, 05, 18, 00, 00), ZoneOffset.ofHours(-4));    System.out.println("-4偏移量工夫为:" + offsetDateTime);    // 转换为ZonedDateTime的示意模式    System.out.println("ZonedDateTime的示意模式:" + offsetDateTime.toZonedDateTime());    System.out.println("ZonedDateTime的示意模式:" + offsetDateTime.atZoneSameInstant(ZoneId.of("America/New_York")));    System.out.println("ZonedDateTime的示意模式:" + offsetDateTime.atZoneSimilarLocal(ZoneId.of("America/New_York")));}输入:-4偏移量工夫为:2021-05-05T18:00-04:00ZonedDateTime的示意模式:2021-05-05T18:00-04:00ZonedDateTime的示意模式:2021-05-05T18:00-04:00[America/New_York]ZonedDateTime的示意模式:2021-05-05T18:00-04:00[America/New_York]

看到了吧,偏移量变为了-4。感触到夏令时的“威力”了吧。

OffsetDateTime和ZonedDateTime的区别

LocalDateTime、OffsetDateTime、ZonedDateTime这三个哥们,LocalDateTime好了解,个别都没有异议。然而很多同学对OffsetDateTime和ZonedDateTime傻傻分不清,这里说说它俩的区别。

  1. OffsetDateTime = LocalDateTime + 偏移量ZoneOffset;ZonedDateTime = LocalDateTime + 时区ZoneId
  2. OffsetDateTime能够随便设置偏移值,但ZonedDateTime无奈自在设置偏移值,因为此值是由时区ZoneId管制的
  3. OffsetDateTime无奈反对夏令时等规定,但ZonedDateTime能够很好的解决夏令时调整
  4. OffsetDateTime得益于不变性个别用于数据库存储、网络通信;而ZonedDateTime得益于其时区个性,个别在指定时区里显示工夫十分不便,无需认为干涉规定
  5. OffsetDateTime代表一个瞬时值,而ZonedDateTime的值是不稳固的,须要在某个刹时依据过后的规定计算出来偏移量从而确定理论值

总的来说,OffsetDateTime和ZonedDateTime的区别次要在于ZoneOffset和ZoneId的区别。如果你只是用来传递数据,请应用OffsetDateTime,若你想在特定时区里做工夫显示那么请务必应用ZonedDateTime。

总结

本着回绝浅尝辄止的态度,深度分析了很多同学可能不太熟悉的OffsetDateTime、ZonedDateTime两个API。总而言之,想要真正把握日期工夫体系(不限于Java语言,而是所有语言,甚至日常生活),对时区、偏移量的理解是绕不过来的砍,这块常识有所欠缺的敌人可往前翻翻补补课。

最初在应用它们三的过程中,有两个揭示给你:

  1. 所有日期/工夫都是不可变的类型,所以若须要比拟的话,请不要应用==,而是用equals()办法。
    2、任何时候,结构一个日期工夫(包含它们三)请永远务必显示的指定时区,哪怕是默认时区。这么做的目标就是明确代码的用意,打消语义上的不确定性。比方若没指定时区,那到底是写代码的人欠考虑了呢,还是就是想用默认时区呢?总之显示指定绝大部分状况下比隐式“指定”语义上好得多。

本文思考题

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

  1. 如何用LocalDateTime形容美国纽约本地工夫?
  2. OffsetDateTime和ZonedDateTime你到底该应用谁?
  3. 一个人的生日应该用什么Java类型存储呢?

举荐浏览

GMT UTC CST ISO 夏令时 工夫戳,都是些什么鬼?

全网最全!彻底弄透Java解决GMT/UTC日期工夫

寰球城市ZoneId和UTC工夫偏移量的最全对照表

关注我

分享、成长,回绝浅尝辄止。关注【BAT的乌托邦】回复关键字专栏有Spring技术栈、中间件等小而美的纯原创专栏。本文已被 https://www.yourbatman.cn 收录。

本文所属专栏:JDK日期工夫,公号后盾回复专栏名即可获取全部内容。

A哥(YourBatman):Spring Framework/Boot开源贡献者,Java架构师。十分重视基本功涵养,置信底层根底决定上层建筑,坚实基础能力焕发程序员更强生命力。文章特点为以小而美专栏模式重构常识体系,抽丝剥茧,致力于做人人能看懂的最好的专栏系列。可加我好友(fsx1056342982)共勉哦!

System.out.println("点个赞吧!");print_r('关注【BAT的乌托邦】!');var_dump('点个赞吧!');NSLog(@"关注【BAT的乌托邦】!");console.log("点个赞吧!");print("关注【BAT的乌托邦】!");printf("点个赞吧!");cout << "关注【BAT的乌托邦】!" << endl;Console.WriteLine("点个赞吧!");fmt.Println("关注【BAT的乌托邦】!");Response.Write("点个赞吧!");alert("关注【BAT的乌托邦】!");echo("点个赞吧!");