日期时间处理一团糟
answer by Teo 的第一段非常有见地且正确:Java 中的日期时间处理是一团糟。我知道的所有其他语言和开发环境也是如此。日期时间工作既困难又棘手,尤其容易出错和令人沮丧,因为我们直观地认为它是日期时间。但在数据类型、数据库、序列化、本地化、跨时区调整以及计算机编程附带的所有其他形式方面,“直观地”并没有削减它。
不幸的是,计算机行业基本上选择忽略这个日期时间工作的问题。鉴于明显的需求,Unicode 花了很长时间才被发明出来,业界也在努力解决日期时间处理问题。
不要依赖 Count-Since-Epoch
但我必须不同意它的结论。使用 count-since-epoch 并不是最好的解决方案。使用 count-since-epoch 本质上是令人困惑、容易出错且不兼容的。
我们创建数字数据类型是为了做数学而不是使用位。我们创建字符串类来处理处理文本而不是裸八位字节的细节。所以我们也应该创建数据类型和类来处理日期时间值。
早期的 Java 团队(以及之前的 IBM 和 Taligent)尝试使用 java.util.Date 和 java.util.Calendar 以及相关类。不幸的是,这种尝试是不充分的。虽然日期时间本质上令人困惑,但这些类增加了更多的混乱。
乔达时间
据我所知,Joda-Time 项目是第一个以彻底、称职和成功的方式处理日期时间的项目。即便如此,Joda-Time 的创造者们并不完全满意。他们继续在 Java 8 中创建 java.time package,并使用 threeten-extra project 扩展该工作。 Joda-Time 和 java.time 具有相似的概念,但又各有优势。
数据库问题
具体来说,java.util.Date 和 .Calendar 类缺少没有时间和时区的仅日期值。而且它们缺少没有日期和时区的仅时间值。在 Java 8 之前,Java 团队添加了称为 java.sql.Date 和 java.sql.Time 类的黑客攻击,这是一个伪装成仅日期的日期时间值。 Joda-Time 和 java.time 都通过提供 LocalDate 和 LocalTime 类来解决这个问题。
另一个具体问题是 java.util.Date 的分辨率为毫秒,但数据库经常使用微秒或纳秒。为了弥合这种差异,早期的 Java 团队不明智地尝试创建另一个 hack,java.sql.Timestamp 类。虽然在技术上是 java.util.Date 子类,但它还跟踪小数秒到纳秒的分辨率。因此,在转换进出这种类型时,您可能会失去或获得更精细的小数秒粒度,而不会意识到这一事实。所以这可能意味着您期望相等的值不相等。
另一个混淆来源是 SQL 数据类型TIMESTAMP WITH TIME ZONE。该名称用词不当,因为不存储时区信息。将名称视为TIMESTAMP WITH RESPECT FOR TIME ZONE,因为任何传递的时区偏移信息都用于将日期时间值转换为UTC。
具有纳秒分辨率的 java.time 包具有一些特定功能,可以更好地与数据库通信日期时间数据。
我可以写更多,但是可以通过在 StackOverflow 中搜索 joda、java.time、sql timestamp 和 JDBC 等词来收集这些信息。
使用带有Postgres 的 JDBC 的 Joda-Time 示例。 Joda-Time 将immutable objects 用于thread-safety。因此,我们不是更改实例(“mutate”),而是根据原始值创建一个新实例。
String sql = "SELECT now();";
…
java.sql.Timestamp now = myResultSet.getTimestamp( 1 );
DateTime dateTimeUtc = new DateTime( now , DateTimeZone.UTC );
DateTime dateTimeMontréal = dateTimeUtc.withZone( DateTimeZone.forID( "America/Montreal" ) );
关注世界标准时间
在此之前,我认为时间戳始终采用 UTC 格式。为什么会有人想要一个本地化的时间戳而不是它的本地化表示?这不会让每个人都感到很困惑吗?
确实如此。 SQL 标准定义了一个TIMESTAMP WITHOUT TIME ZONE,它忽略并删除任何包含的时区数据。我无法想象它的用处。这位 Postgres 专家 David E. Wheeler,says as much in recommending 始终使用 TIMESTAMP WITH TIME ZONE。 Wheeler 引用了一个狭隘的技术例外(分区),即便如此,他还是说在保存到数据库之前自己将所有值转换为 UTC。
最佳做法是使用 UTC 格式处理和存储数据,同时调整到本地时区以便向用户展示。有时您可能想记住本地时区中的原始日期时间数据;如果是这样,保存该值除了转换为UTC。
指南
更好地处理日期时间的第一步是避免使用 java.util.Date 和 .Calendar,使用 Joda-Time 和/或 java.time,专注于 UTC,并了解特定 JDBC 驱动程序的行为和特定的数据库(尽管有 SQL 标准,但数据库在日期时间处理方面差异很大)。
MySQL
警告:我不使用 MySQL(我是 Postgres 类型的人)。
根据version 8 documentation,DATETIME 和TIMESTAMP 这两种类型的不同之处在于第一种缺乏任何时区或与UTC 偏移的概念。第二个使用任何时区指示或从 UTC 偏移的输入,以将该值调整为 UTC,然后将其存储,并丢弃区域/偏移信息。
所以这两种类型似乎类似于标准 SQL 类型:
- MySQL
DATETIME ≈ SQL 标准 TIMESTAMP WITHOUT TIME ZONE
- MySQL
TIMESTAMP ≈ SQL 标准 TIMESTAMP WITH TIME ZONE
对于 MySQL DATETIME,使用 Java 类 LocalDateTime。该类与该数据类型一样,故意缺少任何时区或与 UTC 偏移的概念。使用此类型和类:
- 当您指的是 任何 区域或 所有 区域时,例如“圣诞节从 2018 年 12 月 25 日的第一刻开始”。这意味着不同地方的不同时刻,因为东方比西方更早出现新的一天。
- 当将约会或活动安排在足够远的未来时,政客们可能会更改时区的偏移量,世界各地的政客们对此都表现出一种倾向。在这种用法中,您必须在运行时应用时区来动态计算但不存储用于在日历上显示的时刻。这样,即使政客将时钟重新定义为提前或落后分钟/小时,8 个月内 15:00 的牙科预约仍然保持在 15:00。
对于 MySQL TIMESTAMP,使用 Java 类 Instant,如上所示。将此类型和类用于时刻、时间轴上的特定点。
JDBC 4.2
从 JDBC 4.2 起,我们可以直接与数据库交换 java.time 对象。使用getObject & setObject 方法。
myPreparedStatement.setObject( … , Instant.now() ) ;
检索。
Instant instant = myResultSet.getObject( … , Instant.class ) ;
JDBC 4.2 规范要求驱动程序支持OffsetDateTime,但奇怪的是不需要支持更常见的类型Instant 和ZonedDateTime。但是类型之间的转换非常容易。
OffsetDateTime odt = myResultSet.getObject( … , OffsetDateTime.class ) ;
Instant instant = odt.toInstant() ;
然后,您可以将 Instant 中的 UTC 值调整为特定时区,以便呈现给用户。
ZoneId z = ZoneId.of( "Pacific/Auckland" ) ;
ZonedDateTime zdt = instant.atZone( z ) ;
关于java.time
java.time 框架内置于 Java 8 及更高版本中。这些类取代了麻烦的旧 legacy 日期时间类,例如 java.util.Date、Calendar 和 SimpleDateFormat。
Joda-Time 项目现在位于maintenance mode,建议迁移到java.time 类。
要了解更多信息,请参阅Oracle Tutorial。并在 Stack Overflow 上搜索许多示例和解释。规格为JSR 310。
您可以直接与您的数据库交换 java.time 对象。使用符合JDBC 4.2 或更高版本的JDBC driver。不需要字符串,不需要java.sql.* 类。
从哪里获取 java.time 类?
ThreeTen-Extra 项目通过附加类扩展了 java.time。该项目是未来可能添加到 java.time 的试验场。您可以在这里找到一些有用的类,例如Interval、YearWeek、YearQuarter 和more。