我知道经过 3 年的软件开发后这可能听起来很尴尬,但我正在尝试更好地理解 java.time 类以及如何使用它们来避免错误和不良实践。
在我参与的第一个项目中,使用的 java 版本是 8,但日期值存储在遗留类 java.util.Date 上
两年后,新公司、新项目使用 java 版本 17,但日期值仍然存储在遗留类 java.util.Date 中
当我看到项目上有一个 DateUtils ,其方法如下:
public static String toString_ddMMyyyy_dotted(Date date){
LocalDateTime localDateTime = date.toInstant().atZone(ZoneId.systemDefualt()).toLocalDateTime();
DateTimeFormatter.ofPattern("dd.MM.yyyy").format(localDateTime);
}
public static Date getBeginOfTheYear(Date date){
LocalDate localDate = date.toInstant().atZone(ZoneId.systemDefualt()).toLocalDate();
LocalDate beginOfTheYear = localDate.withMonth(1).withDayOfMonth(1);
return Date.from(beginOfTheYear.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant());
}
所以我的问题是,为什么不直接使用具有非常好的 API 的 java.time 类,而不是在需要对日期进行操作时将变量转换为 LocalDate/LocalDateTime ?
我们的想法是尽快重构项目,将 Date/DateTime/Time 值存储到 java.time 类中。我从一个简单的模块开始,它将一些数据保存到 MongoDB 中。我应用了以下字段类型转换:
- 将日期值转换为 LocalDate
- 日期和时间值转换为 LocalDateTime
在测试过程中我立即发现一个问题,值为 2023-10-07 12:00:00 的 localDateTime 变量在 MongoDB 上保存为 2023-10-07 10:00:00 只要当数据被取回到java中,该值又回到了2023-10-07 12:00:00,但是这并没有发生,所以这是一个大问题。
首先,这种情况是仅在使用LocalDateTime时发生,还是在使用LocalDate时也会发生?
在实体/文档类上存储日期/日期时间值的最佳实践是什么?我应该使用 ZonedDateTime 而不是 LocalDateTime 吗?
转换而不是改造
根据代码库的大小和复杂性,这可能相当危险。
更保守的方法是在java.time中进行所有新编程。要与尚未更新到java.time 的旧代码进行互操作,请进行转换。您会在旧类上找到新的转换方法。寻找
to…
&from…
方法。这些转换方法提供了完整的覆盖范围,让您可以来回转换。修改代码时要注意的另一件事是:许多/大多数程序员对日期时间处理了解甚少。这个话题出人意料地棘手和复杂,第一次遇到时概念很模糊。在处理编程和数据库的严格性时,我们对日期时间的日常理解实际上对我们不利。所以要小心那些糟糕的代码、有缺陷的代码、错误处理日期时间的代码。每次发现此类错误代码时,您都会遇到麻烦,因为生成的报告可能显示不正确的结果,数据库可能存储无效数据等。
您可以搜索现有的 Stack Overflow 问题和答案以了解更多信息。以下是一些可以帮助指导您的简短要点。
瞬间与非瞬间
不正确。或者说不完整,我应该说。
我建议你这样思考……时间追踪有两种:
片刻
时刻是时间轴上的特定点。自 UTC 1970 年第一个时刻的纪元参考以来, java.time中的时刻以整秒计数的形式进行跟踪,加上以纳秒计数的小数秒的计数。“in UTC”是“与 UTC 时间子午线的零时-分-秒偏移”的缩写。
表示时刻的基本类是
java.time.Instant
。此类的对象表示以 UTC 表示的时刻,且始终以 UTC 表示。此类是jav.time框架的基本构建块。这门课应该是你处理时刻时第一个也是最后一个想到的。用于Instant
跟踪时刻,除非您的业务规则特别涉及特定区域或偏移量。并被java.util.Date
专门替换为java.time.Instant
,两者都代表 UTC 中的时刻,但Instant
纳秒分辨率比java.util.Date
毫秒分辨率更精细。另外两个类跟踪java.time中的时刻:
OffsetDateTime
&ZonedDateTime
。OffsetDateTime
表示与 UTC 的偏移量不为零时所看到的时刻。偏移量只是比 UTC 早或晚的小时数、分钟数、秒数。因此,巴黎的人们将墙上的时钟设置为比 UTC 早一到两个小时,而东京的人们将时钟设置为比 UTC 早九小时,而加拿大新斯科舍省的人们将时钟设置为比 UTC 晚三到四个小时。时区不仅仅是一个偏移量。时区是特定地区人民所使用的偏移量的过去、现在和未来变化的命名历史,由其政治家决定。时区的名称格式为&
Continent/Region
等。Europe/Paris
Asia/Tokyo
要通过特定区域的挂钟时间查看日期和时间,请使用
ZonedDateTime
。请注意,您可以通过它们的、和方法轻松、干净地在这三个类
Instant
、 、OffsetDateTime
和之间来回移动。它们提供了三种观察同一时刻的方式。通常您在业务逻辑和数据存储和数据交换中使用,同时用于呈现给用户。ZonedDateTime
to…
at…
with…
Instant
ZonedDateTime
不是片刻
另一方面,我们有“没有一刻”的追踪。这些是不定类型。
造成最混乱的非时刻类是
LocalDateTime
. 此类表示带有一天中的时间但缺少任何偏移量或时区概念的日期。因此,如果我说明年 2024 年 1 月 23 日中午,LocalDateTime.of( 2024 , Month.JANUARY , 23 , 12 , 0 , 0 , 0 )
您无法知道我指的是东京的中午、图卢兹的中午,还是美国俄亥俄州托莱多的中午——三个截然不同的时刻,相隔几个小时。如有疑问,请勿使用
LocalDateTime
。通常,在编写业务应用程序时,我们关心时刻,因此很少使用LocalDateTime
.当我们在商业应用程序中最常需要时
LocalDateTime
,最大的例外是预约,例如牙医预约。在那里,我们需要分别LocalDateTime
保存 a和 time zone(ZoneId
)而不是将它们组合成一个。原因很关键:政治家经常改变时区规则ZonedDateTime
,而且他们的行为是不可预测的,而且往往没有任何预警。因此,如果那里的政治家决定采用夏令时 (DST),或决定放弃 DST,或决定全年保持 DST(最迟),那么下午 3 点的牙医预约可能会提前一小时或晚一小时。时尚),或决定不同的偏移量可能会带来商机(例如:冰岛采用零偏移量)或解决政治问题(例如:整个印度的一个单一时区),或决定调整其时钟作为相对于的外交举措邻国,或者必须采用入侵/占领军的抵消措施。所有这些发生的频率比大多数人(包括程序员)意识到的要多得多。此类更改不必要地破坏了简单编写的应用程序。其他不定类型包括:
LocalDate
对于仅日期值,没有日期时间,也没有任何区域/偏移量。LocalTime
对于仅时间值,没有任何日期,也没有任何区域/偏移。这
LocalDate
可能很棘手,因为许多人没有意识到,在任何特定时刻,全球各地的日期都会因时区而异。现在,日本东京是“明天”,而加拿大埃德蒙顿则是“昨天”。你的代码
该代码有两个问题。
LocalDateTime
。java.util.Date
var后面的类代表date
偏移量为零的时刻。当转换为LocalDateTime
.ZoneId.systemDefualt()
. 这意味着结果会根据 JVM 当前的默认时区而有所不同。如上所述,这意味着日期可能会有所不同,而不仅仅是一天中的时间。在两台不同机器上运行的相同代码可能会产生两个不同的日期。这可能是您在应用程序中想要的,或者对于不知情的程序员来说这可能是一个粗鲁的觉醒。你的另一段代码也有问题。
首先,请注意,不幸的是,遗留日期时间类中有两个 类:一会儿,假装代表仅日期,但实际上是一个时刻(在一个非常糟糕的类设计决策中)。我会承担你的手段。
Date
java.util.Date
java.sql.Date
java.util.Date
其次,避免使用此类代码的此类调用链。可读性、调试和日志记录都变得更加困难。在进行一系列转换时,请编写简单的短行。在每行上使用注释来证明操作的合理性,并解释您的动机。
var
并且,出于同样的原因避免使用;进行此类转换时使用显式返回类型。是
date.toInstant()
好的。遇到 a 时java.util.Date
,立即转换为 aInstant
。同样,使用 意味着
ZoneId.systemDefualt()
代码的结果会因任何系统管理员或更改默认时区的用户的突发奇想而异。这可能是这段代码的作者的意图,但我对此表示怀疑。当您不传递任何参数时,该部分
beginOfTheYear.atStartOfDay()
会生成 a 。LocalDateTime
因此,您再次丢弃了有价值的信息(抵消),而没有获得任何回报。另一个问题:您的代码甚至无法编译。该
java.util.Date.from
方法采用 anInstant
,而不是LocalDateTime
您的调用返回的abeginOfTheYear.atStartOfDay()
。为了正确获得特定时刻的第一时刻,您几乎肯定会希望决定业务规则的人规定特定的时区。
您确实应该将传入转换
java.util.Date
为Instant
.然后应用该区域。
提取年份部分。如上所述,日期可能因时区而异。因此,年份可能会有所不同,具体取决于您在上面的前一行代码中提供的区域!
获取该年的第一天。
让java.time确定该区域当天的第一个时刻。不要假设开始时间是 00:00。某些区域中的某些日期在其他时间开始,例如 01:00。
ZoneId
请注意,在确定一年中的第一个时刻时,我们传递了to的参数atStartOfDay
,这与您的代码不同。这里的结果是 aZonedDateTime
而不是LocalDateTime
代码中的 the 。最后,我们转换为
java.util.Date
. 在绿地代码中,我们会像瘟疫一样避免此类。但在您现有的代码库中,我们必须进行转换,以便与尚未更新到java.time的旧代码部分进行互操作。MongoDB
你说:
那里有太多的东西需要解开,而你提供的细节太少。
我建议您专门针对该问题发布另一个问题。提供足够的详细信息以及示例代码来进行诊断。