提问人:Paul Marcelin Bejan 提问时间:10/8/2023 最后编辑:Paul Marcelin Bejan 更新时间:10/10/2023 访问量:213
如何正确使用java.time类?
How to use java.time classes correctly?
问:
我知道经过 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 类,而不是在需要对 Date 进行操作时将变量转换为 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 吗?
答:
转换而不是改造
这个想法是尽快重构项目,将 Date/DateTime/Time 值存储到 java.time 类中
根据代码库的大小和复杂性,这可能是相当危险的。
一种更保守的方法是在 java.time 中进行所有新的编程。要与尚未更新到 java.time 的旧代码进行互操作,请转换。您将在旧类上找到新的转换方法。查找 & 方法。这些转换方法提供完整的覆盖范围,让您可以来回切换。to…
from…
修改代码时需要注意的另一句话:许多/大多数程序员对日期时间处理的理解很差。这个话题出奇地棘手和复杂,第一次遇到这些概念时很滑。在处理编程和数据库的严格性时,我们对日期时间的日常理解实际上对我们不利。因此,要小心糟糕的代码,错误的代码,它们会错误地处理日期时间。每次发现此类错误代码时,您都会遇到麻烦,因为可能生成了显示错误结果的报告,数据库可能存储无效数据等。
您可以搜索现有的 Stack Overflow 问题和答案以了解更多信息。以下是一些简短的要点,可帮助您指导您。
时刻与非时刻
将日期值转换为 LocalDate
将日期和时间值转换为 LocalDateTime
不對。或者不完整,我应该说。
我建议你这样构建你的思维......有两种时间跟踪:
- 力矩(确定)
- 不是一刻(无限期)
时刻
时刻是时间轴上的一个特定点。自 UTC 中 1970 年第一个时刻的纪元引用以来,时刻在 java.time 中以整秒计数,加上小数秒以纳秒计数进行跟踪。“in UTC”是“与UTC时间子午线的偏移量为零小时-分钟-秒”的缩写。
表示时刻的基本类是 。此类的对象表示在 UTC 中看到的时刻,始终以 UTC 表示。这个类是 jav.time 框架的基本构建块。这门课应该是你在处理时刻时的第一个也是最后一个想法。用于跟踪某个时刻,除非您的业务规则专门涉及特定区域或偏移量。并特别替换为 ,两者都表示 UTC 中的某个时刻,但比毫秒的分辨率更精细,达到纳秒。java.time.Instant
Instant
java.util.Date
java.time.Instant
Instant
java.util.Date
另外两个类在 java.time 中跟踪时刻:& .OffsetDateTime
ZonedDateTime
OffsetDateTime
表示与 UTC 偏移量不为零时所看到的时刻。偏移量仅比 UTC 提前或晚几小时/分钟-秒。因此,巴黎的人们将时钟设置在UTC前一两个小时,而东京的人们将时钟设置在九小时前,而加拿大新斯科舍省的人们则将时钟设置在UTC前三到四个小时。
时区不仅仅是一个偏移量。时区是特定地区人民根据其政治家决定的偏移量的过去、现在和未来变化的命名历史。时区的名称格式为 & 。Continent/Region
Europe/Paris
Asia/Tokyo
要通过特定区域的挂钟时间查看日期和时间,请使用 。ZonedDateTime
请注意,您可以轻松干净地在这三个类之间来回移动,以及通过它们的 、 和 方法。它们提供了三种看待同一时刻的方式。通常,您在业务逻辑和数据存储和数据交换中使用,同时用于向用户演示。Instant
OffsetDateTime
ZonedDateTime
to…
at…
with…
Instant
ZonedDateTime
不是片刻
另一方面,我们有“没有片刻”的跟踪。这些是不确定类型。
最引起混淆的 not-a-moment 类是 。此类表示具有时间的日期,但缺少任何偏移量或时区的概念。因此,如果我说 2024 年 1 月 23 日中午,你无法知道我指的是东京的中午、图卢兹的中午还是美国俄亥俄州托莱多的中午——三个截然不同的时刻相隔几个小时。LocalDateTime
LocalDateTime.of( 2024 , Month.JANUARY , 23 , 12 , 0 , 0 , 0 )
如有疑问,请勿使用 .通常,在编写业务应用程序时,我们关心时刻,因此很少使用 .LocalDateTime
LocalDateTime
最大的例外是,当我们最常需要在商业应用程序中时,是预约,例如牙医。在那里,我们需要单独保存 和 时区 (),而不是将它们组合成一个 .原因至关重要:政客们经常改变时区规则,而且他们这样做是不可预测的,而且往往几乎没有预警。因此,如果那里的政客决定,下午3点的牙医任命可能会提前一小时或晚一个小时:LocalDateTime
LocalDateTime
ZoneId
ZonedDateTime
- 采用夏令时 (DST)。
- 放弃 DST。
- 全年保持夏令时(最新的时尚)。
- 不同的抵消将带来商机。示例:冰岛采用偏移量为零。
- 不同的抵消将产生政治影响。例如:印度在广阔的次大陆上建立了一个单一的时区。
- 调整他们的时钟作为相对于邻国的外交举动。示例:朝鲜更改其偏移量以匹配韩国。
- 采用入侵/占领军的偏移。
所有这些发生的频率都比大多数人意识到的要高得多。这样的更改会不必要地破坏幼稚的应用程序。
其他不确定类型包括:
LocalDate
对于没有时间且没有任何区域/偏移量的仅日期值。LocalTime
对于没有任何日期和任何区域/偏移量的仅时间值。
这可能很棘手,因为许多人不知道,在任何给定的时刻,日期在全球范围内因时区而异。现在是日本东京的“明天”,同时也是加拿大埃德蒙顿的“昨天”。LocalDate
您的代码
LocalDateTime localDateTime = date.toInstant().atZone(ZoneId.systemDefualt()).toLocalDateTime();
该代码有两个问题。
- 一是使用不当。var 后面的类表示偏移量为零的时刻。在转换为 时,您丢弃了关键信息,即偏移量。
LocalDateTime
java.util.Date
date
LocalDateTime
- 另一个问题是使用 .这意味着结果会因 JVM 的当前默认时区而异。如上所述,这意味着日期可能会有所不同,而不仅仅是一天中的时间。在两台不同计算机上运行的相同代码可能会生成两个不同的日期。这可能是你想要的应用程序,或者这可能是对一个不知情的程序员的粗鲁唤醒。
ZoneId.systemDefualt()
您的另一段代码也有问题。
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());
}
首先,请注意,不幸的是,在旧版日期时间类中有两个类:一会儿,它假装代表一个仅日期,但实际上是一个时刻(在一个非常糟糕的类设计决策中)。我会承担你的手段.Date
java.util.Date
java.sql.Date
java.util.Date
其次,避免使用此类代码的此类调用链。可读性、调试和日志记录都变得更加困难。在进行一系列转换时,请改为编写简单的短行。在每一行上使用注释来证明操作的合理性,并解释您的动机。并且,出于同样的原因避免使用;执行此类转换时,请使用显式返回类型。var
这很好。当遇到 时,立即转换为 .date.toInstant()
java.util.Date
Instant
同样,使用意味着代码的结果会因任何更改默认时区的系统管理员或用户的心血来潮而有所不同。这可能是此代码作者的意图,但我对此表示怀疑。ZoneId.systemDefualt()
当您不传递任何参数时,部件 beginOfTheYear.atStartOfDay()
会生成 LocalDateTime
。因此,您再次丢弃了有价值的信息(偏移量),而没有获得任何回报。
另一个问题:你的代码甚至无法编译。该方法采用 Instant,而不是调用 beginOfTheYear.atStartOfDay()
返回的 Instant
。java.util.Date.from
LocalDateTime
为了正确地获得特定时刻的一年中第一个时刻的第一个时刻,您几乎肯定希望决定业务规则的人决定特定的时区。
ZoneId zTokyo = ZoneId.of( "Asia/Tokyo" ) ;
您确实应该将传入的 .java.util.Date
Instant
Instant instant = myJavaUtilDate.toInstant() ;
然后应用该区域。
ZonedDateTime zdt = instant.atZone( zTokyo ) ;
提取年份部分。如上所述,日期可能因时区而异。因此,年份可能会有所不同,具体取决于您在上面的上一行代码中提供的区域!
Year year = Year.from( zdt ) ;
获得那一年的第一天。
LocalDate firstOfYear = year.atDay( 1 ) ;
让 java.time 确定该区域中当天的第一个时刻。不要假设开始时间是 00:00。某些区域中的某些日期从其他时间开始,例如 01:00。
请注意,在确定一年中的第一个时刻时,我们将参数传递给 ,这与代码相反。此处的结果是代码中的 而不是 。ZoneId
atStartOfDay
ZonedDateTime
LocalDateTime
ZonedDateTime zdtFirstMomentOfTheYearInTokyo = firstOfYear.atStartOfDay( zTokyo ) ;
最后,我们转换为 .在绿地代码中,我们会像瘟疫一样避免这个类。但是在您现有的代码库中,我们必须转换以与旧代码中尚未更新到 java.time 的部分进行互操作。java.util.Date
Instant instantFirstMomentOfTheYearInTokyo = zdtFirstMomentOfTheYearInTokyo.toInstant();
java.util.Date d = java.util.Date.from( instantFirstMomentOfTheYearInTokyo ) ;
MongoDB数据库
你说:
在测试过程中,我立即发现了一个问题,一个值为 2023-10-07 12:00:00 的 localDateTime 变量被保存在 MongoDB 上为 2023-10-07 10:00:00 只要将数据提取回 java 时,值回到 2023-10-07 12:00:00,这不是问题,但这并没有发生,所以这是一个大问题。
太多了,无法解开,你的细节太少了。
我建议你专门针对这个问题发布另一个问题。提供足够的详细信息以及示例代码以进行诊断。
评论
LocalDateTime
java.util.Date