关于java:LocalDateTimeOffsetDateTimeZonedDateTime互转这一篇绝对喂饱你

2次阅读

共计 11321 个字符,预计需要花费 29 分钟才能阅读完成。

你好,我是 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 等等。然而请记住,如果没有偏移量 / 时区等附加信息,一个工夫是 不能 示意工夫线上的某一时刻的。

代码示例

最大 / 最小值:

@Test
public 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:00
LocalDateTime 最大值:+999999999-12-31T23:59:59.999999999
-999999999-1-1
999999999-12-31

结构:

@Test
public 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 指的是纽约,细品这句话。

计算:

@Test
public 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

格式化:

@Test
public 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 时区,不同中央获取到瞬时值也有可能不一样

代码示例

最大 / 最小值:

@Test
public 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:00
OffsetDateTime 最大值:+999999999-12-31T23:59:59.999999999-18:00
+18:00:-999999999-1-1
-18:00:999999999-12-31

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

结构:

@Test
public 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

计算:

格式化:

@Test
public 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

@Test
public 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

@Test
public 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:00
LocalDateTime 的示意模式: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 来管制的。

代码示例

结构:

@Test
public 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

@Test
public 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

@Test
public 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:00
ZonedDateTime 的示意模式:2021-01-17T19:43:28.320-04:00
ZonedDateTime 的示意模式: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 号,也就是处于夏令营期间):

@Test
public 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:00
ZonedDateTime 的示意模式:2021-05-05T18:00-04:00
ZonedDateTime 的示意模式: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("点个赞吧!");
正文完
 0