提问人:MKorsch 提问时间:8/22/2023 最后编辑:MKorsch 更新时间:8/23/2023 访问量:114
Timestamp#toInstant TimeZone 行为
Timestamp#toInstant TimeZone behaviour
问:
我目前很难将 a 转换为 .我知道(和)不知道任何时区。我有一个表示 UTC 时间的时间戳。我想将此时间戳转换为我的本地时区 (GMT+2)。一旦我打电话给我,我就会得到一个少于 2 小时的约会时间。我认为这是因为 Instant 代表了 UTC 时间尺度上的一个时间点。显然 java 认为我的时间戳在我的默认时区 (= GMT+2),这就是为什么从原始时间戳中减去两个小时的原因。但这已经代表了 UTC 时间,因此从它减去两个小时是错误的。java.sql.Timestamp
LocalDateTime
Timestamp
Date
#toInstant()
Timestamp
toInstant()
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);
但是在这个工作解决方案中,不涉及时间戳。
在前两个答案之后编辑:
- 在我遇到问题的生产代码中,Timestamp 不是通过已弃用的构造函数构造的,而是通过 spring data CrudRepository 和 jakarta.persitence @Entity从数据库中检索的。这就是为什么我不太确定时间戳最终是如何构建的,但这也是一个我不想问自己的问题。
new Timestamp(123, 7, 1, 10, 0, 0, 0)
- 如上一句话所述,我正在使用现有的代码库,其中持久性层的重构目前不是一个可行的解决方案。这就是为什么我必须坚持使用,即使我知道并理解避免使用旧的日期 API 而使用新的 java.time API 是首选解决方案。
Timestamp
- 我的同事终于为我的具体问题找到了解决方案:
@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.sql.Timestamp
延伸。后者是一个谎言——它根本不代表日期。它表示自纪元以来没有时区的时间,从这个意义上说,包中的直接等价物实际上是 ,最好这样想。java.util.Date
java.time
java.time.Instant
鉴于时间类型(因此,所有时间类型,因为它们都扩展!)由于对它们的性质感到困惑而基本上被破坏了(例如被命名,嗯,),你不应该使用这些类型。无论何处。曾。java.util
java.sql
j.u.Date
Date
Date
做对了
幸运的是,您不必使用损坏的类型。没有必要惹.曾经。JDBC5 规范要求 JDBC 驱动程序分别支持 SQL 类型 、 和 的现代类型。因此,假设您有一个表,其中有一个名为 type 的列,您当前可能有这种代码:java.sql.Timestamp
LocalDate
LocalTime
LocalDateTime
OffsetDateTime
java.time
DATE
TIME WITHOUT TIMEZONE
TIMESTAMP WITHOUT TIMEZONE
TIMESTAMP WITH TIMEZONE
person
birthdate
DATE
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
这避免了所有问题。
直接回答您的问题
你真的应该跳过这一部分,只是做对了。查找对 、 、 等的所有调用 - 并正确修复它们。getTimestamp
getDate
setTimestamp
setDate
因为你问题的正确答案是:没关系,你需要更深入一层,停止使用这些东西。
但是,为了满足好奇心:
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 值的概念是人类的发明,这是微妙不正确的。这不是它在引擎盖下的工作方式。当你忽视这一点时,你会感到困惑。
即时是一回事:它表示毫自纪元以来,而不是天、月、小时等。因此,就人类推算而言,与这些类型的任何交互,无论是您在传递人工推算的地方调用的构造函数,还是 Instant
的 toString()
输出,都只是为了人类的方便,可能会造成混淆,并且是接触点内部转换的结果 - 内部没有任何东西被存储为人类对这些类型的清算!
知道我们可以弄清楚为什么你会得到“奇怪”的结果:“有帮助”的人工推算构造函数假设你正在平台默认时区中编写你的人工推算(显然,对于你的系统来说,目前相对于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/Amsterdam
CEST
如果你没有时区,或者这不是你想要表示的,那么根本就不能使用,相反,你需要使用 ,这是唯一可以在没有时区的情况下正确表示人类推算的类型!Timestamp
LocalDateTime
如果你想要一个很好的编程接口来表示你想要的东西的构造实例,请在过程的后期使用并转换为损坏的(、、等)类型:java.sql.Timestamp
java.time
java.sql.Timestamp
java.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 应用平台本地时区,无论您是否愿意。
评论
忘掉java.sql.TIMESTAMP
rzwitserloot 的答案是完全正确的。但让我更简单地说:
👉🏽 停止使用时间戳
。
Java 有两个用于日期时间处理的框架:(a) 添加到早期版本的 Java 中一组存在严重缺陷的类,以及 (b) 添加到 Java 8 中的 java.time 类。第一个现在是遗产,完全被第二个取代。
这些旧版类包括 .因此,请停止使用该类 Timestamp
。java.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 标准类型的列的类。
OffsetDateTime
TIMESTAMP WITH TIME ZONE
- 类 对于 SQL 标准类型的列。
LocalDateTime
TIMESTAMP WITHOUT TIME ZONE
如果通过尚未更新到 java.time 的代码传递对象,请立即转换为 。Timestamp
java.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 ) ;
为了向用户演示,应用时区 () 以获得 .从那里,使用诸如 生成的文本来生成文本。ZoneId
ZonedDateTime
DateTimeFormatter
DateTimeFormatter.ofLocalizedDateTime
评论
java.time
java.time
时间戳
(和日期
)不知道任何时区。我有一个表示 UTC 时间的时间戳。...这是错误的:.不,它表示默认时区的 10:00。所以你的转换是正确的。//this represents 2023-08-01 10:00:00 (UTC)
Instant
Timestamp
Timestamp
Timestamp
LocalDateTime
OffsetDateTime