Timestamp#toInstant TimeZone 行为

Timestamp#toInstant TimeZone behaviour

提问人:MKorsch 提问时间:8/22/2023 最后编辑:MKorsch 更新时间:8/23/2023 访问量:114

问:

我目前很难将 a 转换为 .我知道(和)不知道任何时区。我有一个表示 UTC 时间的时间戳。我想将此时间戳转换为我的本地时区 (GMT+2)。一旦我打电话给我,我就会得到一个少于 2 小时的约会时间。我认为这是因为 Instant 代表了 UTC 时间尺度上的一个时间点。显然 java 认为我的时间戳在我的默认时区 (= GMT+2),这就是为什么从原始时间戳中减去两个小时的原因。但这已经代表了 UTC 时间,因此从它减去两个小时是错误的。java.sql.TimestampLocalDateTimeTimestampDate#toInstant()TimestamptoInstant()Timestamp

我尝试了以下方法:

Timestamp stamp = new Timestamp(123, 7, 1, 10, 0, 0, 0); //this represents 2023-08-01 10:00:00 (UTC)
stamp.toInstant() //returns 2023-08-01 08:00:00

我想要的是:2023-08-01 12:00:00 我能够得到以下结果:

Instant nowUtc = Instant.now();
ZoneId europeZone = ZoneId.of("Europe/Berlin");
ZonedDateTime berlinTime = ZonedDateTime.ofInstant(nowUtc, europeZone);

但是在这个工作解决方案中,不涉及时间戳。

在前两个答案之后编辑:

  1. 在我遇到问题的生产代码中,Timestamp 不是通过已弃用的构造函数构造的,而是通过 spring data CrudRepository 和 jakarta.persitence @Entity从数据库中检索的。这就是为什么我不太确定时间戳最终是如何构建的,但这也是一个我不想问自己的问题。new Timestamp(123, 7, 1, 10, 0, 0, 0)
  2. 如上一句话所述,我正在使用现有的代码库,其中持久性层的重构目前不是一个可行的解决方案。这就是为什么我必须坚持使用,即使我知道并理解避免使用旧的日期 API 而使用新的 java.time API 是首选解决方案。Timestamp
  3. 我的同事终于为我的具体问题找到了解决方案:
@Test
    fun `convert LocalDateTime to Timestamp and back`() {
        val timestamp = Timestamp.valueOf(LocalDateTime.now(ZoneOffset.UTC))
        println("BEFORE $timestamp")
        val localDateTime = ZonedDateTime.of(timestamp.toLocalDateTime(), ZoneOffset.UTC)
            .withZoneSameInstant(ZoneId.of("Europe/Berlin")).toLocalDateTime()
        println("AFTER $localDateTime")
    }

这将打印(取决于执行时间): 之前 2023-08-22 14:53:34.085801 在 2023-08-22T16:53:34.085801 之后

java 日期时间

评论

2赞 Jon Skeet 8/22/2023
我怀疑问题在于您使用的(已弃用的)Timestamp 构造函数实际上假定本地时间而不是 UTC。该文档没有提供任何相关信息,但它似乎与您所看到的和我在测试中看到的一致。尝试使用未弃用的构造函数,改用 milliseconds-since-unix-epoch 值...
0赞 Chaosfire 8/22/2023
@JonSkeet 你可能是对的。有问题的构造函数调用超级(也已弃用)Date 构造函数,该构造函数假定本地时区。构造函数的文档
3赞 Arvind Kumar Avinash 8/22/2023
当您拥有现代 API 时,为什么要使用容易出错且过时的旧日期时间 API?您可以查看此答案和此答案,了解如何将 API 与 JDBC 一起使用。java.timejava.time
0赞 Anonymous 8/23/2023
当你说......时间戳(和日期)不知道任何时区。我有一个表示 UTC 时间的时间戳。...这是错误的:.不,它表示默认时区的 10:00。所以你的转换是正确的。//this represents 2023-08-01 10:00:00 (UTC)Instant
0赞 Anonymous 8/23/2023
你从哪里得到一个物体?我希望你没有真正使用那个 7-arg 构造函数。它已被弃用超过 25 年,正是因为它在跨时区工作不可靠。你能从一开始就避免得到一个物体吗?如果从数据库中检索,请检索现代或相反。TimestampTimestampTimestampLocalDateTimeOffsetDateTime

答:

3赞 rzwitserloot 8/22/2023 #1

上下文

java.sql.Timestamp延伸。后者是一个谎言——它根本不代表日期。它表示自纪元以来没有时区的时间,从这个意义上说,包中的直接等价物实际上是 ,最好这样想。java.util.Datejava.timejava.time.Instant

鉴于时间类型(因此,所有时间类型,因为它们都扩展!)由于对它们的性质感到困惑而基本上被破坏了(例如被命名,嗯,),你不应该使用这些类型。无论何处。曾。java.utiljava.sqlj.u.DateDateDate

做对了

幸运的是,您不必使用损坏的类型。没有必要惹.曾经。JDBC5 规范要求 JDBC 驱动程序分别支持 SQL 类型 、 和 的现代类型。因此,假设您有一个表,其中有一个名为 type 的列,您当前可能有这种代码:java.sql.TimestampLocalDateLocalTimeLocalDateTimeOffsetDateTimejava.timeDATETIME WITHOUT TIMEZONETIMESTAMP WITHOUT TIMEZONETIMESTAMP WITH TIMEZONEpersonbirthdateDATE

try (
  var preparedStatement = con.prepareStatement("SELECT birthdate FROM person";
  var query = preparedStatement.executeQuery()) {

  while (query.next()) {
    java.sql.Date d = query.getDate(1);  
    doStuffWith(d);
  }
}

该代码已过时且危险。相反,请将违规行(调用)替换为:.getDate

java.time.LocalDate d = query.getObject(1, LocalDate.class):

javadoc 指出,您传递的类型是否“有效”取决于 JDBC 驱动程序,但如前所述,需要 JDBC5 兼容驱动程序来支持它。getObject

您也可以使用(在填充 PreparedStatements 的值或 INSERTing 数据时)传递实例。.setObject()LocalDate

这避免了所有问题。

直接回答您的问题

你真的应该跳过这一部分,只是做对了。查找对 、 、 等的所有调用 - 并正确修复它们。getTimestampgetDatesetTimestampsetDate

因为你问题的正确答案是:没关系,你需要更深入一层,停止使用这些东西。

但是,为了满足好奇心:

Timestamp stamp = new Timestamp(123, 7, 1, 10, 0, 0, 0); //this > represents 2023-08-01 10:00:00 (UTC)
stamp.toInstant() //returns 2023-08-01 08:00:00

不,这都是非常不正确的。你对日期和时间的理解是错误的。

时间戳不能表示人工计算(即年、月、日等)。它表示millis-since-epoch,用天、小时、分钟(和 UTC 时区)来指代一些 millis-since-epoch 值的概念是人类的发明,这是微妙不正确的。这不是它在引擎盖下的工作方式。当你忽视这一点时,你会感到困惑。

即时是一回事:它表示毫自纪元以来,而不是天、月、小时等。因此,就人类推算而言,与这些类型的任何交互,无论是您在传递人工推算的地方调用的构造函数,还是 InstanttoString() 输出,都只是为了人类的方便,可能会造成混淆,并且是接触点内部转换的结果 - 内部没有任何东西被存储为人类对这些类型的清算!

知道我们可以弄清楚为什么你会得到“奇怪”的结果:“有帮助”的人工推算构造函数假设你正在平台默认时区中编写你的人工推算(显然,对于你的系统来说,目前相对于UTC有+02:00的偏移量,即例如夏季欧洲大陆的大部分地区)。java.sql.Timestamp

因此,它尽职尽责地将你的人类计算转换为代表布鲁塞尔某人“嘿,你能给期和时间吗?”时,代表那个确切的时刻,会得到答案:“当然,伙计!现在是 2023 年 8 月 1 日,正好是早上 10:00!它不存储时区,也不存储“10 点钟”的概念。它只是存储那个大数字:你的对象有一个值的字段,这就是它的全部(过度简化一点,有更多的字段,但它们对这个结论没有有意义的影响)。它不存储时区,也不存储人工推算版本(2023/08/01/10/00/00)。1690876800000

然后你要求它自己打印,这会导致它1690876800000转换回人类的计算形式。由于您在要求它自己打印时没有提供时区,并且它不存储时区,因此行为取决于您正在执行的操作:

theInstanceOfJavaSqlTimestamp.toString()

生成字符串值“2023-08-01 10:00:00.0”,它这样做是因为它的 toString “有用地”使用您的平台默认时区,该时区仍然是 .这可能会让你认为它尽职尽责地存储了“10 点钟”——事实并非如此。Europe/Amsterdam

相反,Instant 的“更好”之处在于它试图避免混淆,并避免给人一种它正在存储时间戳信息的印象:

theInstanceOfJavaSqlTimestamp.toInstant().toString()

调用只是给你一个瞬间,其中带有 epoch-millis 的一个字段仍然具有相同的1690876800000值存储在里面。然而,它的 impl 选择通过将其即时转换为人工推算来呈现此值,就像 'toString() 一样,但它使用 UTC 而不是“平台本地时区”。toInstant()toString()java.sql.Timestamp

这并不能改变这个简单的事实:这两个对象代表了完全相同的时间点。这只是对“方法应该如何向人类调试器显示有用信息”的不同看法。toString()

我想要的是:2023-08-01 12:00:00 我能够得到结果:

这从根本上是错误的 - Instant 和 Timestamp 根本不是这样工作的。这个问题是荒谬的。您的时间戳代表1690876800000。这就是它所代表的全部内容。鉴于我们谈论的是纪元毫,唯一明智的方式是:我希望它代表[这里的人类计算值],如果你包括一个完整的时区(风格,而不是风格,这不足以唯一地确定时间和日期)。Europe/AmsterdamCEST

如果你没有时区,或者这不是你想要表示的,那么根本就不能使用,相反,你需要使用 ,这是唯一可以在没有时区的情况下正确表示人类推算的类型!TimestampLocalDateTime

如果你想要一个很好的编程接口来表示你想要的东西的构造实例,请在过程的后期使用并转换为损坏的(、、等)类型:java.sql.Timestampjava.timejava.sql.Timestampjava.util.Date

Instant nowUtc = Instant.now();
ZoneId europeZone = ZoneId.of("Europe/Berlin");
ZonedDateTime berlinTime = ZonedDateTime.ofInstant(nowUtc, europeZone);

这段代码可以写得更简单:

ZoneId europeZone = ZoneId.of("Europe/Berlin");
ZonedDateTime berlinTime = ZonedDateTime.now(europeZone);

将其转换为 a 没有意义,因为 j.s.Timestamp 不包含时区信息。如果数据库列这样做,那么唯一正确的方法是使用 。不得不使用 OffsetDateTime 是痛苦的,但这是 SQL 在时区方面也被混淆和破坏的结果(它认为偏移量是时区。它们不是,称它们为时区非常令人困惑,但是,我们在这里)。您可以尝试使用 ZDT 实例进行调用,但如果这不起作用,则必须调用:Timestamp.setObject(colIdxOrName, instanceOfOffsetDateTime).setObject()

ZoneId europeZone = ZoneId.of("Europe/Berlin");
ZonedDateTime berlinTime = ZonedDateTime.now(europeZone);
preparedStatement.setObject(berlinTime.toOffsetDateTime());

如果您必须以 Timestamp 形式使用它,请注意时区是正确的,此代码稍后会巧妙地破坏您。最接近的:

Instant now = Instant.now();
// FORCIBLY SET YOUR SYSTEM TO BERLIN TIME!
// STUFF WILL BREAK IF SYSTEM TZ IS EVER CHANGED!
java.sql.Timestamp ts = java.sql.Timestamp.from(now);

j.s.Timestamp 应用平台本地时区,无论您是否愿意。

评论

0赞 VGR 8/23/2023
一个非常小的点:没有必要将 ResultSet 放在 try-with-resources 语句中,因为关闭语句会关闭该语句的 ResultSet
1赞 Basil Bourque 8/22/2023 #2

忘掉java.sql.TIMESTAMP

rzwitserloot 的答案是完全正确的。但让我更简单地说:

👉🏽 停止使用时间戳

Java 有两个用于日期时间处理的框架:(a) 添加到早期版本的 Java 中一组存在严重缺陷的类,以及 (b) 添加到 Java 8 中的 java.time 类。第一个现在是遗产,完全被第二个取代。

这些旧版类包括 .因此,请停止使用该类 Timestampjava.sql.Timestamp

UTC 值

您说您想要 2023-08-01 12:00:00 (UTC) 的值。

传递各个部分。

LocalDateTime ldt = LocalDateTime.of( 2023 , 8 , 1 , 12 , 0 , 0 ) ;
OffsetDateTime odt = ldt.atOffset( ZoneOffset.UTC ) ;

或者,分析一个字符串,其中末尾表示与 UTC 的偏移量为 0 小时-分钟-天。Z

Instant instant = Instant.parse ( "2023-08-01T12:00:00Z" ) ;

数据库

对于 JDBC 4.2+ 中的数据库工作,请使用:

  • SQL 标准类型的列的类。OffsetDateTimeTIMESTAMP WITH TIME ZONE
  • 类 对于 SQL 标准类型的列。LocalDateTimeTIMESTAMP WITHOUT TIME ZONE

如果通过尚未更新到 java.time 的代码传递对象,请立即转换为 。Timestampjava.time.Instant

Instant instant = myJavaSqlTimestamp.toInstant() ;

若要将该值发送到 SQL 标准类型的数据库列,请执行以下操作:TIMESTAMP WITH TIME ZONE

OffsetDateTime odt = instant.atOffset( ZoneOffset.UTC ) ;
myPreparedStatement.setObject( … , odt ) ;

检索:

OffsetDateTime odt = myResultSet.getObject( … , OffsetDateTime.class ) ;

为了向用户演示,应用时区 () 以获得 .从那里,使用诸如 生成的文本来生成文本。ZoneIdZonedDateTimeDateTimeFormatterDateTimeFormatter.ofLocalizedDateTime