在Java中操作和存储日期的最佳实践是什么?

31

在企业级Java应用程序中,使用GregorianCalendar处理和存储日期的最佳实践是什么?

希望得到反馈,并将任何优秀答案整合成其他人可以使用的最佳实践。


7
嘟嘟嘟,joda时间,嘟嘟嘟... - Pascal Thivent
1
这个回答解决了您的问题吗?Java处理/存储具有地理多样性用户的日期最佳做法 - erhanasikoglu
5个回答

27
通常最佳实践并不是考虑使用庞大的日期对象,而是存储一个时间点。这通常通过存储一个值来实现,该值不会受到极端情况或潜在解析问题的影响。为此,人们通常会存储自“时代”(1970年1月1日)以来经过的毫秒数(或秒数)。这是非常普遍的做法,任何Java API都将始终允许您将任何类型的日期转换为/从自纪元以来以毫秒表示的时间。
这是用于存储的方式。如果有这样的需要,您还可以存储例如用户首选时区等信息。
现在,如下所示,可以使用以毫秒为单位的日期:
System.out.println( System.currentTimeMillis() );
1264875453

在向最终用户显示时,时间戳并不是非常有用,这是肯定的。

这就是为什么你要使用例如 Joda time 这样的工具,在向最终用户显示之前将其转换为一些用户友好的格式。

你询问了最佳实践,这是我的看法:将“日期”对象存储在数据库中,而不是以毫秒为单位的时间,就像使用浮点数表示货币金额一样。

这通常是一个很大的代码异味。

因此,在Java中使用Joda时间来操作日期是正确的方式,但是使用Joda来存储日期吗?当然不是


13

Joda 时间库 是首选。为什么呢?

  1. 它比标准的日期/时间 API 更强大、更直观。
  2. 使用 Joda 进行日期/时间格式化不会出现线程问题。而 java.text.SimpleDateFormat 不是线程安全的(很少有人知道这一点!)

在某个时候,Java 日期/时间 API 将被 JSR-310 取代。我相信这将基于 Joda 制作,因此您将学习一个在新标准 Java API 中影响力的 API。


现在,如果您无法使用Joda(某些项目受限于引入到项目中的外部JAR),那么使用java.util.GregorianCalendar来保存和操作日期(确保通过像在UTC时区中维护日期这样的方式考虑DST)是最佳实践吗? - BestPractices
1
FYI,Joda-Time 项目现已进入维护模式,团队建议迁移到 java.time 类。 - Basil Bourque
真的吗?我不知道。感谢您提醒。 - Brian Agnew
我认为在任何情况下,将重型第三方库添加到项目中都不能被称为最佳实践。 - Евгений Шевченко
@ЕвгенийШевченко 原始的日期时间类确实是如此糟糕,设计不良,令人困惑和有缺陷,以至于对于我们许多人来说,将Joda-Time jar添加到任何新项目中确实是最佳实践。幸运的是,现在Joda-Time的继承者java.time类已经内置于Java 8和Java 9中。对于Java 6和7,应该添加ThreeTen-Backport项目的库。该后移端口进一步适用于早期Android的ThreeTenABP项目。 - Basil Bourque

11

Joda time(与JDK完全兼容)

Joda-Time提供了Java日期和时间类的高质量替代品。其设计支持多个日历系统,同时提供简单的API接口。


(Note: I kept the HTML tags intact as requested)

1
FYI,Joda-Time 项目现已进入维护模式,团队建议迁移到 java.time 类。 - Basil Bourque

10

协调世界时

在编程中,不要考虑任何时区,而是在协调世界时中思考、工作和存储数据。将协调世界时视为“唯一真实时间”,而所有其他时区都只是变化。因此,在编码时,请忘记自己所在的时区。在协调世界时中进行业务逻辑、日志记录、数据存储和数据交换。我建议每个程序员在他们的桌子上放置第二个设置为协调世界时的时钟。

java.time

现代化的方式是使用java.time类。

提到的Joda-Time项目为java.time类提供了灵感,该项目现已进入维护模式,并建议团队迁移到java.time类。

java.time框架已内置于Java 8及更高版本中。这些类取代了老旧的legacy日期时间类,例如java.util.Date, .Calendarjava.text.SimpleDateFormat等。

要了解更多信息,请查看Oracle教程。在Stack Overflow上搜索许多示例和解释。规范是JSR 310
如何获取java.time类?

ThreeTen-Extra项目扩展了java.time的额外类。该项目是java.time可能未来添加的一个试验场。您可能会在这里找到一些有用的类,例如Interval, YearWeek, YearQuarter, 以及更多

ISO 8601

在将日期时间值序列化为文本时,请使用ISO 8601标准。

例如,UTC中的日期时间为2016-10-17T01:24:35Z,其中Z代表Zulu,表示UTC。对于其他相对于UTC的偏移量,小时和分钟的偏移量会出现在末尾,例如2016-01-23T12:34:56+05:30。java.time类扩展了此标准格式,以方括号形式附加时区名称(如果已知),例如2016-01-23T12:34:56+05:30[Asia/Kolkata]
标准还有许多其他方便的格式,包括持续时间, 间隔, 序数, 和 年-周

数据库

对于数据库存储,请使用日期时间类型来存储日期时间值,例如SQL标准数据类型,这些类型主要有DATETIMETIMESTAMP WITH TIME ZONE

让您的JDBC驱动程序来处理繁琐的细节。驱动程序处理有关介绍和适应Java如何处理数据以及数据库如何在其一侧处理数据的内部的琐碎细节。但务必通过示例数据进行实践,以了解驱动程序和数据库的行为。 SQL标准对于日期时间处理定义很少,因此行为差异很大,出乎意料。

如果使用符合JDBC 4.2及更高版本的JDBC驱动程序,则可以直接通过ResultSet :: getObjectPreparedStatement :: setObject方法获取和存储java.time类型。

Instant instant = myResultSet.getObject( … );
myPreparedStatement.setObject( … , instant );

对于旧驱动程序,您需要通过java.sql类型进行转换。查找添加到旧类中的新转换方法。例如,java.sql.Timestamp.toInstant()
Instant instant = myResultSet.getTimestamp( … ).toInstant();
myPreparedStatement.setObject( … , java.sql.Timestamp.from( instant ) );

使用java.sql类型时应尽可能简洁。它们是一个糟糕设计的hack,例如java.sql.Date伪装成仅日期值,但实际上作为java.util.Date的子类,确实将时间设置为UTC中的00:00:00。以及,请忽略继承的事实,根据类文档。一团糟。

示例代码

获取当前UTC时间。

Instant instant = Instant.now();

以上展示了将Instant对象存储到数据库中并从中提取的方法。

要生成ISO 8601字符串,只需调用toString。Java.time类默认使用ISO 8601格式来解析和生成各种日期时间值的字符串。

String output = instant.toString();

将任何偏移量转换为 ZoneOffset 并获取 OffsetDateTime 。调用 toString 以生成ISO 8601格式的字符串。
ZoneOffset offset = ZoneOffset.ofHoursMinutes( 5 , 30 );
OffsetDateTime odt = instant.atOffset( offset );

一个时区是一个偏移量加上一组处理异常情况的规则,例如夏令时(DST)。当你需要通过某个地区自己的挂钟时间来观察同一时刻时,请应用时区(ZoneId)以获取一个ZonedDateTime对象。
大陆/地区格式指定一个正确的时区名称。永远不要使用三到四个字母的缩写,如ESTIST,因为它们不是真正的时区,不标准化,甚至不唯一(!)。
ZoneId z = ZoneId.of( "Asia/Kolkata" );
ZonedDateTime zdt = instant.atZone( z );

如果你想从一个OffsetDateTime或者ZonedDateTime中提取出一个Instant,你可以调用toInstant方法。

Instant instant = zdt.toInstant();

格式化

如果要将时间以 ISO 8601 以外的其他格式呈现给用户,请搜索 Stack Overflow 使用 DateTimeFormatter 类。

虽然您可以指定自定义格式,但通常最好让 java.time 自动本地化。为了本地化,请指定:

  • FormatStyle 来确定字符串应该有多长或缩写。
  • Locale 来确定 (a) 翻译日、月名称等人类语言和 (b) 决定缩写、大写、标点符号等文化规范。

示例:

Locale l = Locale.CANADA_FRENCH ; 
DateTimeFormatter f = DateTimeFormatter.ofLocalizedDateTime( FormatStyle.FULL ).withLocale( l );
String output = zdt.format( f );

转换

尽可能避免使用旧的日期时间类型。但如果需要使用旧代码,还没有更新为java.time类型,可以进行java.time类型的转换。详情请参见问题将java.util.Date转换为哪种“java.time”类型?

使用对象

使用对象而不是简单的编码原始数据和字符串。例如:

  • 不要使用1-7来表示星期几,使用DayOfWeek枚举,例如DayOfWeek.TUESDAY
  • 与其传递一个日期字符串,不如传递LocalDate对象。
  • 与其传递一对整数表示年份和月份,不如传递YearMonth对象。
  • 月份不要使用1-12,而是使用更易读的Month枚举,例如Month.JANUARY
使用这样的对象可以使您的代码更加自我说明,确保有效值,并提供类型安全。type-safety

-2

为了开始讨论,这是我的经验:

在创建典型的三层Java企业项目的标准时,我通常建议使用GregorianCalendar来操作日期。原因是GregorianCalendar是事实上的标准,优于任何其他日历实例,例如Julian日历等。它是大多数国家公认的日历,并正确处理闰年等。除此之外,我建议应用程序将其日期存储为UTC,以便您可以轻松执行日期计算,例如查找两个日期之间的差异(如果它存储为EST,例如,您必须考虑夏令时)。然后可以将日期本地化为您需要显示给用户的任何时区,例如将其本地化为EST,如果您是美国东海岸公司,并且希望将时间信息显示为EST。


3
“例如儒略历等”这句话可以忽略,因为它并没有意义。关于日期的特殊情况太多了,不可能在这里全部列举。比如,天文学家使用天文儒略日编号,他们也使用公历日期。最好这方面说得少一些。此外,对于你的每个要点最好使用项目符号。 - S.Lott
1
一个三层架构的项目与其他项目有何不同? - jontejj
FYI,旧的日期时间类(如java.util.Datejava.util.Calendarjava.text.SimpleTextFormat)现在已经成为遗留系统,被Java.time类所取代。 - Basil Bourque

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接