【问题标题】:Why is the following date conversion in Java 8 not appropriate?为什么 Java 8 中的以下日期转换不合适?
【发布时间】:2026-02-14 08:25:08
【问题描述】:

我看过很多关于以下日期转换的争论:

timeStamp.toLocalDateTime().toLocalDate();

有人说不合适,因为必须指定时区才能正确转换,否则结果可能会出乎意料。我的要求是我有一个包含 Timestamp 字段的对象和另一个包含 LocalDate 字段的对象。我必须考虑两者之间的日期差异,所以我认为最好使用的常用类型是 LocalDate。我不明白为什么必须将时区指定为时间戳或 LocalDate 仅代表日期。时区已经隐含。当这种转换失败时,有人可以举个例子吗?

【问题讨论】:

  • yyyy-mm-dd hh:mm:ss
  • 上下文会很有帮助 - 没有时区的时间值有问题 - 它是存储在存储它的人的本地时间还是在 UTC(或其他一些商定的时区)? “一些”人可能会抱怨它的原因是因为很难量化它的实际价值。是根本错误吗?我不这么认为,但你需要考虑到你对系统的了解。就个人而言,我会确保两件事之一。时区与值一起存储,或者所有时间值都存储在给定/已知时区
  • (1) 你知道你想要哪个时区吗?那些人是对的:如果你不这样做,你就不能确定你正在进行正确的转换(无论你如何转换)。 (2) Timestamp 是否来自数据库(当Timestamp时尚 时,这本来是预期用途)?如果是这样,来自timestamp with time zonetimestamp 类型的字段(没有时区)或datetime

标签: java date


【解决方案1】:

比这更复杂。虽然Timestamp 确实是一个时间点,但它也往往具有双重性质,有时它会伪装成日期和时间。

顺便说一句,您可能已经知道,Timestamp 类设计不佳且早已过时。最好能完全避免。如果您从旧版 API 获得 Timestamp,那么您做的是正确的事:立即将其转换为来自 java.time(现代 Java 日期和时间 API)的类型。

时间戳是一个时间点

要将时间点(无论如何表示)转换为日期,您需要确定时区。在所有时区,它永远不会是同一个日期。所以时区的选择总是会有所作为。所以一个正确的转换是:

    ZoneId zone = ZoneId.of("Africa/Cairo");
    LocalDate date = timestamp.toInstant().atZone(zone).toLocalDate();

Timestamp 类设计用于您的 SQL 数据库。如果您在 SQL 中的数据类型是timestamp with time zone,那么它明确表示一个时间点,您需要将其视为刚刚描述的一个时间点。即使对于大多数数据库引擎timestamp with time zone 真的只是意味着“UTC 时间戳”,它仍然是一个时间点。

再说一遍:有时被认为是一天中的日期和时间

来自Timestamp的文档:

时间戳还提供格式化和解析操作以支持 时间戳值的 JDBC 转义语法。

JDBC 转义语法定义为

yyyy-mm-dd hh:mm:ss.fffffffff,其中fffffffff 表示 纳秒。

这并没有定义任何时间点。这只是一天中的日期和时间。文档甚至没有告诉您的是,日期和时间是在 JVM 的默认时区中理解的。

我想以这种方式看到Timestamp 的原因来自SQL Timestamp 数据类型。在大多数数据库引擎中,这是一个没有时区的日期和时间。这不是时间戳,尽管有名字!它没有定义时间点,这就是时间戳的目的和定义。

我见过很多情况,Timestamp 打印的日期和时间与数据库中的相同,但并不代表数据库中隐含的时间点。例如,可能决定数据库中的“时间戳”采用 UTC,而 JVM 使用其运行地点的时区。这是一种不好的做法,但不会在几年内消失。

这也一定是Timestamp 配备了您在问题中使用的toLocalDateTime 方法的原因。它为您提供数据库中的日期和时间,对吗?所以在这种情况下,你在问题中的转换应该是正确的,或者......?

正如其他人已经提到的,当 JVM 的默认时区发生更改时,这可能会在我们没有机会注意到的情况下惨遭失败。 JVM 的默认时区可以随时从您的程序或运行在同一 JVM 中的任何其他程序的任何位置更改。发生这种情况时,您的 Timestamp 对象不会更改它们的时间点,但它们确实会默认更改它们的时间,有时也会更改它们的日期。我在 Stack Overflow 问题和其他地方读过关于错误结果和由此产生的混乱的恐怖故事。

解决方法:不要使用时间戳

从 JDBC 4.2 开始,您可以从 SQL 数据库中检索 java.time 类型。如果您的 SQL 数据类型是 timestamp with time zone(建议用于时间戳),请获取 OffsetDateTime。一些 JDBC 驱动程序还允许您获取 Instant,这也很好。在这两种情况下,时区更改都不会对您产生任何影响。如果 SQL 类型是 timestamp 没有时区(不鼓励且太常见),请获取 LocalDateTime。同样,您可以确保无论 JVM 时区设置是否更改,您的对象都不会更改其日期和时间。只有您的 LocalDateTime 从未定义过时间点。转换为 LocalDate 很简单,正如您在问题中已经证明的那样。

链接

【讨论】:

    【解决方案2】:

    如您所见(取自https://*.com/a/32443004/1398418):

    Timestamp 代表 UTC 中的一个时刻,相当于现代的 Instant

    当你这样做时:

    timeStamp.toLocalDateTime().toLocalDate();
    

    timeStamp 从 UTC 转换为系统时区。和做的一样:

    timeStamp.toInstant().atZone(ZoneId.systemDefault()).toLocalDate()
    

    例如:

    Timestamp stamp = new Timestamp(TimeUnit.HOURS.toMillis(-1)); // UTC 1969-12-31
    System.setProperty("user.timezone", "EET"); // Set system time zone to Eastern European EET - UTC+2
    stamp.toLocalDateTime().toLocalDate(); // represents EET 1970-01-01
    stamp.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); // represents EET 1970-01-01
    

    该结果(获取系统时区中的日期)是预期的,如果这是您想要的,那么执行timeStamp.toLocalDateTime().toLocalDate() 是适当且正确的。

    您是说您在某个对象中有一个 LocalDate 字段,并且您希望在它和 Timestamp 之间有一个句点,如果没有其他信息,这是不可能的。 LocalDate 只是代表一个日期,它没有时区信息,你需要知道它是如何创建的以及使用的时区。

    如果它表示系统时区中的日期,则使用timeStamp.toLocalDateTime().toLocalDate() 获取期间是正确的,如果它表示 UTC 或任何其他时区中的日期,那么您可能会得到错误的结果。

    例如,如果 LocalDate 字段代表 UTC 日期,您将需要使用:

    timeStamp.toInstant().atZone(ZoneId.of("UTC")).toLocalDate();
    

    【讨论】:

    【解决方案3】:

    示例:1 月 23 日变为 24 日

    你问:

    当这种转换失败时,有人可以举个例子吗?

    是的,我可以。

    从 1 月 23 日开始。

    LocalDate ld = LocalDate.of( 2020 , Month.JANUARY , 23 );
    LocalTime lt = LocalTime.of( 23 , 0 );
    ZoneId zMontreal = ZoneId.of( "America/Montreal" );
    ZonedDateTime zdt = ZonedDateTime.of( ld , lt , zMontreal );
    Instant instant = zdt.toInstant();
    

    zdt.toString() = 2020-01-23T23:00-05:00[美国/蒙特利尔]

    instant.toString() = 2020-01-24T04:00:00Z

    Instant 类代表 UTC 中的时刻。让我们使用添加到旧类的新转换方法转换为非常旧的类java.sql.Timestamp

    // Convert from modern class to troubled legacy class `Timestamp`. 
    java.sql.Timestamp ts = Timestamp.from( instant );
    

    ts.toString() = 2020-01-23 20:00:00.0

    不幸的是,Timestamp::toString 方法在生成文本时会动态应用 JVM 的当前默认时区。

    ZoneOffset defaultOffset = ZoneId.systemDefault().getRules().getOffset( ts.toInstant() );
    System.out.println( "JVM’s current default time zone: " + ZoneId.systemDefault() + " had an offset then of: " + defaultOffset );
    

    JVM 当前的默认时区:America/Los_Angeles 当时的偏移量为:-08:00

    所以Timestamp::toString 在从凌晨 4 点调回 8 小时到晚上 8 点后误报了对象的 UTC 值。这个反特性是这个设计不佳的类的几个严重问题之一。有关Timestamp 的诡异行为的更多讨论,请参阅正确的Answer by Ole V.V.

    让我们运行您的代码。想象一下在运行时 JVM 当前的默认时区是Asia/Tokyo

    TimeZone.setDefault( TimeZone.getTimeZone( "Asia/Tokyo" ) );
    LocalDate localDate = ts.toLocalDateTime().toLocalDate();
    

    测试是否相等。哎呀!我们最终得到了 24 号而不是 23 号。

    boolean sameDate = ld.isEqual( localDate );
    System.out.println( "sameDate = " + sameDate + " | ld: " + ld + " localDate: " + localDate );
    

    sameDate = 假 | ld: 2020-01-23 localDate: 2020-01-24

    看到这个code run live at IdeOne.com


    那么你的代码有什么问题?

    • 切勿使用java.sql.Timestamp。它是 Java 最早版本附带的几个糟糕的日期时间类之一。 永远不要使用这些遗留类。它们已被 JSR 310 中定义的现代 java.time 类完全取代。
    • 你打电话给toLocalDateTime,它会删除重要信息。删除任何时区或与 UTC 的偏移量,只留下日期和时间。所以这个类不能用来表示一个时刻,不是时间轴上的一个点。例如:2020 年 12 月 25 日中午——是德里的中午、杜塞尔多夫的中午还是底特律的中午,三个不同的时刻相隔几个小时? LocalDateTime 本质上是模棱两可的。
    • 您在确定日期时忽略了时区这一关键问题。对于任何给定的时刻,日期在全球范围内都会有所不同。在某一时刻,在澳大利亚可能是“明天”,而同时在墨西哥可能是“昨天”。

    【讨论】:

      【解决方案4】:

      问题在于这些对象所代表的内容。您的问题忘记了一个关键方面,即:timeStamp 的类型是什么?

      我猜它是一个java.sql.Timestamp 对象。

      时间戳,就像 java.util.Date 一样,是旧 API 等价于 Instant

      它代表时间的瞬间,从 UTC 时间 1970 年 1 月 1 日起以毫秒为单位。系统不知道应该在哪个时区。你应该知道;错误,如果此处将发生错误,则在您访问此代码之前已经发生。以下是它如何出错的简单解释:

      1. 您从用户在网络表单的日期字段中输入日期开始;现在是 2020 年 4 月 1 日。

      2. 您在阿姆斯特丹运行的服务器将其保存到数据库列中,该列在内部表示为 UTC,没有区域。这是一个错误(您不是在保存瞬间,而是在保存日期,这两者不是一回事)。实际存储在数据库中的是阿姆斯特丹 2020 年 4 月 1 日午夜的确切时间(在 UTC 中,这将是前一天的 22:00!)。

      3. 稍后,您将这一时刻及时查询回 java.sql.Timestamp 对象,并且当服务器的 tz 位于其他地方(例如,伦敦时间)时,您正在执行此操作。然后,您将其转换为本地日期时间,并从那里转换为本地日期,然后......您得到 2020-03-31。

      哎呀。

      日期应保留为日期。永远不要将 LocalX(无论是 Time、Date 还是 DateTime)转换为 Instant(或任何有效的瞬间,包括 jsTimestamp 或 juDate - 是的,juDate 不代表日期,它的名称非常糟糕),或者反之亦然,否则会产生疼痛。如果您因为落后的 API 而必须格外小心;很难测试“移动服务器的时区”是否会破坏东西!

      【讨论】:

      • " UTC。系统不知道应该在哪个时区。"如果是UTC,为什么系统不知道时区?
      • @Oleg 两种观点都有效:(1)Timestamp 与时区无关,系统假装它在JVM的默认时区,但实际上不知道。 (2) Timestamp 内部保存自纪元以来的秒数和纳秒数,并且纪元通常以 UTC 定义,因此从这个意义上说,您可能Timestamp 的内部表示在UTC。