Java LocalDateTime 与 ZonedDateTime

Java LocalDateTime vs ZonedDateTime

提问人:horvoje 提问时间:10/24/2023 最后编辑:horvoje 更新时间:10/25/2023 访问量:71

问:

我正在创建一个后端服务,它应该向第三方服务发出一些请求,并根据响应进行更新。单个工作由数千个微小的工作组成。主作业的确切时间和每个子作业的确切时间和以及每个子作业的确切时间和都应存储在报告中。 主作业也可以从 Web 用户界面运行,这意味着其他时区取决于登录的用户。beginendbeginend

最初的想法(和实现)是存储在主作业的开头(如果作业按计划启动,则存储在本地服务器,如果用户手动启动作业,则存储在登录用户的作业),然后用于存储主作业和子作业。还是我应该在这种情况下使用而不是?ZoneIdLocalDateTimebeginendInstantLocalDateTime

private ZoneId zoneId;
private LocalDateTime begin;
private LocalDateTime end;

我不确定这是否是正确的方法,或者我应该为每个方法使用,而忘记独立的财产。ZoneIdZonedDateTimebeginendZoneId

private ZonedDateTime begin;
private ZonedDateTime end;

编辑:只是为了给未来的读者提供更多信息:

案例 1 将是 cron 作业,它将在某个时间每天运行一次(不是那么重要,但每天在同一时间),报告将显示“开始作业”,“完成作业”。

情况 2 是单击“立即运行更新”,相同的作业将运行并生成相同的报告。

java java-time

评论

3赞 Jon Skeet 10/24/2023
使用 LocalDateTime 的一个问题是:它可能不明确。如果您采用第一种设计,区域 ID 为“欧洲/伦敦”,开始/结束值为 2023-10-29T01:15:00 和 2023-10-29T01:45:00,那是 a) 半小时吗?b) 一个半小时;c) 无效?(可能是其中任何一个,因为凌晨 1:15 和凌晨 1:45 分别在即将到来的周日在伦敦发生两次......
2赞 Jon Skeet 10/24/2023
或者,如果作业没有固有时区,并且您希望能够在所需的任何时区中报告该时区,请使用即时。真的,如果没有更多的背景,很难知道该建议什么。
3赞 Jon Skeet 10/24/2023
但是,如果事情在未来被安排得很长,那么使用 Instant 可能会产生一个问题:如果用户将其安排在一年后的(比如)上午 9 点开始,并且时区规则在现在和那时之间发生变化,如果您只记录了一个瞬时,那么您最终可能会在上午 10 点或早上 8 点开始。有关详细信息,请参阅 codeblog.jonskeet.uk/2019/03/27/...。生活是复杂的:(
1赞 Basil Bourque 10/25/2023
Instant.now()是你的朋友。

答:

7赞 rzwitserloot 10/24/2023 #1

LocalDateTime或多或少准确地代表了它的名字。它代表了人类对时间概念的计算(所以,年、月、日、小时——诸如此类的东西。与“millis”相反,这是计算机计算时间概念)。

而且根本没有时区。因此,给定一个对象,除非您可以保证它来自同一时区,否则不可能将其与纪元(例如您从 或其他 LDT 对象获得的纪元)进行比较。在您的问题文本中,您不是 - 这取决于系统是否启动了作业(在这种情况下,您得到系统默认值 tz。也可以改变!),或者用户启动了它(在这种情况下,你会得到他们的 tz)。因此,给定系统中的 2 个 LDT,您无法分辨哪个 LDT 在之前或之后。LocalDateTimeSystem.currentTimeMillis()

您甚至不需要涉及多个 TZ。我们所需要的只是夏令时。

假设我在欧洲/阿姆斯特丹做某事:我开始工作。

您使用 LDT 并尽职尽责地记录它:

ZoneId getUserZone() {
  return ZoneId.of("Europe/Amsterdam");
}

ZoneId zone = getUserZone();
LocalDateTime eventStamp = LocalDateTime.now(zone);
logSystem.write(eventStamp, "User logged in");

半小时后,我注销了。相同的代码。

一天后,您检查日志消息。你见证了这一点:

[2023-March-26 02:40] User logged in
[2023-March-26 02:10] User logged out

什么什么现在??注销事件到底怎么可能在具有较早时间戳的日志文件中?就此而言,为什么日志文件的词法顺序与时间戳顺序不一致?做了...我发明了时间机器吗?

不 - 发生了夏令时。时钟倒退了一个小时。

LDT 不存储任何这些内容。

因此,它们对于精确计时来说是可怕的东西

它们代表了即兴的评论,例如“我出生于 1975 年 4 月 14 日”。一般来说,你不会用“哦,真的吗?那么,那么是哪个时区呢?如果我在4月14日23:00出生在纽约,然后搬到欧洲,那么我不会在15日开始庆祝我的生日。即使我们做精确的时区簿记,我也应该;纽约比欧洲“早”6到7个小时,所以你出生的时候,是欧洲15日凌晨05:00。

尽管如此,您仍然不会更新您的出生日期。因此,如果您将“出生”作为 ZonedDateTime 进行跟踪,然后尝试计算您的出生日期与出生日期之间的年差,那么在 15 日,它会增加 1。这是错误的。ZonedDateTime.now("Europe/Amsterdam")

这是一个语义模型(你的生日是用年、月和日期来表示的——没有时区成分,也没有人像它那样行事),它说:用/来代表我。LocalDateLDT

相比之下,假设您在 2026 年 4 月 30 日下午 1 点在阿姆斯特丹的一家理发店预约。如果您将其存储为 LocalDateTime ,那么如果您不在阿姆斯特丹,那么当您试图告诉您距离理发师预约还有多少小时时,任何闹钟或议程系统都会搞砸。2026-04-30 13:00:00

如果你把它存储为epochmillis,那也是错误的。想象一下(这根本不是异国情调,欧盟已经决定必须实施夏令时,只是还没有时间表)荷兰的政治议事厅敲响了锤子,“从 2025 年开始,我们将永久保持冬季”。假设您有一个时钟,在理发师预约之前,它正在滴答作响。

锤子砰的一声,就是时钟应该将“距离理发师预约的时间”精确调整 1 小时的那一刻。因为你的约会仍然是“2026 年 4 月 30 日下午阿姆斯特丹的 1 点”。但现在要提前一个小时了。

因此,存储约会会尖叫:“使用 ZonedDateTime”。因为 ZDT 正确更新(不是随着锤子落下,而是随着 tzdata 文件更新以反映该政治决定)。

日志事件处于确切的时间点。因此,正确的表示既不是 ZDT 也不是 LDT。它。java.time.Instant

但是,将 Instant 转换为 ZDT 并返回过去发生的事件_for通常是安全的。因此,如果 Instant 不在桌面上,则 ZDT 在语义上是错误的,但不会导致错误。

瞬时也没有时区,但它们确实代表了确切的时间点。这使得它们具有可比性(时钟来回移动并不重要 - 即时根本不“做”时区,而且地球上每个人的 java 虚拟机都返回相同的值,加上减去一些由于时钟不完全正确,当你调用或 .longSystem.currentTimeMillis()Instant.now()

在向用户呈现日志文件时,人类往往非常不善于将“此日志写在1698159191068上”。我们喜欢那些岁月之类的。但是,您存储的语义概念Instant。因此,将其存储起来,并在将其呈现给用户时,将该即时转换为任何合适的内容。例如,在向用户显示日志时,使用用户的已知时区将 Instant 转换为 LDT 并不疯狂。

2赞 Basil Bourque 10/25/2023 #2

听起来您的问题混合了两种截然不同的东西:

  • 安排约会。
  • 跟踪历史记录,某事发生的那一刻,时间轴上的特定点

如果您想将任务安排在下周一下午 3 点:

ZoneId z = ZoneId.of( "America/Edmonton" ) ;
LocalDate today = LocalDate.now( z ) ;
LocalDate nextMonday = today.with( TemporalAdjusters.next( DayOfWeek.MONDAY ) ) ;
LocalTime targetTime = LocalTime.of( 3 , 0 ) ;

如果您尚未准备好确定某个时刻,请存储这三个字段 (, , ),或者将日期和时间一起存储为 )。政客们经常改变其管辖范围内时区的偏移量,而且往往在很少或根本没有预警的情况下这样做。因此,如果在下午 3 点执行任务很重要,请推迟片刻的决定。znextMondaytargetTimeLocalDateTime

要确定一个时刻:

ZonedDateTime zdt = ZonedDateTime.of( nextMonday , targetTime , z ) ;

调整为 UTC(偏移量为零)。

Instant then = zdt.toInstant() ;

确定要经过的时间:

Duration d = Duration.between ( Instant.now() , then ) ;

要记录子任务的实际运行时间,请捕获当前时刻。

Instant start = Instant.now() ;
…
Instant end = Instant.now() ;

在写出文本(例如日志)时,只需调用即可生成标准 ISO 8601 格式的文本。Instant#toString

尽可能使用 UTC(零偏移量)。戴上“程序员”的帽子时,忘记你自己的狭隘时区。在 UTC 中思考、记录和存储。将办公室中的第二个时钟设置为 UTC。仅应用时区 (a) 业务规则要求时,以及 (b) 向用户显示时区。Instant

评论

0赞 horvoje 10/25/2023
谢谢!我现在明白了,我的问题不是很清楚。它不是将两件事混为一谈——两者都是一样的。第一件事应该是 cron 作业,它将在任何时候每天运行一次,报告会说“开始工作”,“完成工作”。第二件事是,当单击“立即运行更新”时,相同的作业将运行并生成相同的报告。我必须坐下来两次阅读这里的所有内容,并决定最佳选择。
0赞 horvoje 10/25/2023
也许我会计算花在工作上的时间并将其显示在报告中,但现在它并不那么重要。