Java SE 8 TemporalAccessor.from 与 java.time.Instant 对象一起使用时出现问题

Posted

技术标签:

【中文标题】Java SE 8 TemporalAccessor.from 与 java.time.Instant 对象一起使用时出现问题【英文标题】:Java SE 8 TemporalAccessor.from issues when used with a java.time.Instant object 【发布时间】:2014-05-13 21:24:31 【问题描述】:

java.time 有一个 Instant 类,它封装了时间轴上的位置(或“时刻”)。虽然我知道这是一个秒/纳秒值,因此与时区或时间偏移没有直接关系,但它的 toString 返回格式为 UTC 日期/时间的日期和时间,例如 2014-05-13T20:05:08.556Z .此外,anInstant.atZone(zone)anInstant.atOffset(offset) 都会产生一个与将 Instant 视为具有隐含的 UTC 时区/“零”偏移量一致的值。

因此,我会期望:

ZoneOffset.from(anInstant) 产生一个“零”ZoneOffset OffsetDateTime.from(anInstant) 生成具有“零”偏移的日期/时间 ZoneId.from(anInstant)(可能)生成 UTC ZoneId ZonedDateTime.from(anInstant)(可能)生成带有 UTC ZoneIdZonedDateTime

ZonedDateTime.from 的文档在我阅读时似乎支持这一点。

事实上,ZoneOffset.from(anInstant)DateTimeException 失败,我想出于这个原因,OffsetDateTime.from(anInstant) 也失败了,其他两个也是如此。

这是预期的行为吗?

【问题讨论】:

【参考方案1】:

简答:

JSR-310 设计者不希望人们通过 ZoneIdZoneOffsetOffsetDateTimeZonedDateTime 等类型的静态 from() 方法在机器时间和人类时间之间进行转换。如果您仔细研究 javadoc,则会明确指定这一点。而是使用:

OffsetDateTime#toInstant():Instant
ZonedDateTime#toInstant():Instant
Instant#atOffset(ZoneOffset):OffsetDateTime
Instant#atZone(ZoneId):ZonedDateTime

静态 from() 方法的问题在于,否则人们可以在 InstantLocalDateTime 之间进行转换,而无需考虑时区。

长答案:

无论将Instant 视为计数器还是字段元组,JSR-310-team 给出的答案是严格区分所谓的机器时间和人工时间。事实上,他们打算进行严格的分离——请参阅他们的guidelines。所以最后他们希望Instant 仅被解释为机器时间计数器。所以他们故意设计了一个你不能向Instant询问年份、小时等字段的设计。

但实际上,JSR-310 团队并不一致。他们已经将方法Instant.toString() 实现为字段元组视图,包括年、...、小时、...和偏移符号 Z(对于 UTC 时区)(脚注:在 JSR-310 之外,这很常见对此类机器时间的基于字段的外观 - 例如参见 Wikipedia 或其他网站上关于 TAI and UTC)。一旦规范负责人 S. Colebourne 在threeten-github-issue 的评论中说:

“如果我们真的很严格,那么 Instant 的 toString 就是从 1970-01-01Z 开始的秒数。我们选择不这样做,并输出一个更友好的 toString 来帮助开发人员,但这并没有改变 Instant 只是秒数的基本事实,并且在没有某种时区的情况下无法转换为年/月/日。”

人们可以喜欢或不喜欢这个设计决定(比如我),但结果是你不能向Instant 询问年份、...、小时、...和偏移量。另请参阅支持字段的文档:

NANO_OF_SECOND 
MICRO_OF_SECOND 
MILLI_OF_SECOND 
INSTANT_SECONDS 

有趣的是这里缺少什么,最重要的是缺少一个与区域相关的字段。 作为一个原因,我们经常听到像Instantjava.util.Date 这样的对象没有时区的说法。 在我看来,这是一种过于简单化的观点。虽然这些对象在内部确实没有时区状态(也不需要具有这样的内部值),但这些对象必须与 UTC 时区相关,因为这是每个时区偏移计算和转换为本地类型的基础.所以正确的答案是:Instant 是一个机器计数器,计算自 UNIX 纪元以来在时区 UTC 中的秒数和纳秒数(根据规范)。最后一部分 - 与 UTC 区域的关系 - JSR-310-team 没有很好地指定,但他们不能否认。 设计者希望从Instant 中取消时区方面,因为它看起来与人类时间相关。 但是,他们不能完全取消它,因为这是任何内部偏移计算的基本部分。所以你对

的观察

“同样,Instant.atZone(zone)Instant.atOffset(offset) 都会产生一个值,该值与将 Instant 视为具有隐含的 UTC 时区/‘零’偏移量是一致的。”

是对的。

虽然ZoneOffset.from(anInstant) 可能会产生ZoneOffset.UTC 可能非常直观,但它会引发异常,因为它的 from() 方法搜索不存在的OFFSET_SECONDS-field。 JSR-310 的设计者出于同样的原因决定在规范中这样做,即让人们认为Instant 与 UTC 时区正式无关,即“没有时区”(但在内部他们必须接受这一点所有内部计算的基本事实!)。

出于同样的原因,OffsetDateTime.from(anInstant)ZoneId.from(anInstant) 也失败了。

关于ZonedDateTime.from(anInstant)我们read:

“转换将首先从时间对象获取 ZoneId,如有必要,将回退到 ZoneOffset。然后它将尝试获取 Instant,如有必要,将回退到 LocalDateTime。结果将是ZoneId 或 ZoneOffset 与 Instant 或 LocalDateTime 的组合。”

因此,由于同样的原因,此转换将再次失败,因为无法从 Instant 获得 ZoneIdZoneOffset。异常消息内容如下:

“无法从 TemporalAccessor 获取 ZoneId:1970-01-01T00:00:00Z of type java.time.Instant”

最后,我们看到所有静态 from() 方法都无法在人类时间和机器时间之间进行转换,即使这看起来很直观。在某些情况下,假设LocalDateInstant 之间的转换是有问题的。指定了这种行为,但我预测您的问题不会是此类问题的最后一个问题,许多用户将继续感到困惑。

我认为真正的设计问题是:

a) 人类时间和机器时间之间不应有明显的区别。像Instant 这样的时间对象应该更好地表现得像两者一样。量子力学中的一个类比:您可以将电子视为粒子和波。

b) 所有静态 from() 方法都太公开了。在我看来,Ihat 太容易访问了,最好从公共 API 中删除或使用比TemporalAccessor 更具体的参数。这些方法的缺点是人们可能会忘记在此类转换中考虑相关的时区,因为他们以本地类型开始查询。例如:LocalDate.from(anInstant)(在哪个时区???)。但是,如果您直接向 Instant 询问其日期,例如 instant.getDate(),我个人会认为 UTC 时区中的日期是有效答案,因为这里的查询是从 UTC 角度开始的。

c) 总之:我绝对与 JSR-310 团队分享避免在不指定时区的情况下在本地类型和全局类型(如 Instant)之间进行转换的好主意。我只是在 API 设计上有所不同,以防止用户进行这种 timezone-less 转换。我更喜欢的方法是限制 from() 方法,而不是说全局类型不应该与日历日期或墙壁时间或 UTC-timezone-offset 等人类时间格式有任何关系。

无论如何,由于保留了向后兼容性,这种(无关紧要的)机器时间和人类时间分离设计现在已经确定下来,每个想要使用新 java.time-API 的人都必须接受它。

抱歉,回答太长了,但很难解释 JSR-310 的选择设计。

【讨论】:

我非常同意你所说的一切。我不太在意您不能在任何其他.from 方法中使用Instant,因为有可行的替代方案,但正如我之前所说,这些事情应该记录在案!抱歉@IdanArye 我不同意你的观点。我已经对 java.time 类中的 from 方法的参数支持 TemporalAccessorobjects 进行了编程分析。 Instant的结果是:Instant.from不支持DayOfWeek, LocalDate, LocalDateTime, LocalTime, Month, MonthDay, OffsetTime, Year,3 ZoneOffset @user3627702 我一直认为Instant 没有日期和时间的概念很有趣,但是为toString() 提供了基于字段的实现,甚至还有一个额外的InstantFormatter .好吧,没有人希望将日志记录时间戳视为机器计数器,因此 JSR-310 团队面临着部分偏离他们自己的机器时间视图的压力。无论如何,您对这种行为的额外文档行的想法非常好。 该设计旨在避免诸如 LocalDate.from(anInstant) 之类的事情,因为这将是许多错误的根源(人们会抱怨这种转换使用 UTC 而不是默认时区,并且英国的开发人员会发现代码在冬天可以工作,但在夏天就不行,所以错误会进入生产阶段) @JodaStephen 是的,完全正确。您如何看待静态 from() 方法中的额外文档行,其中大致包含您对所选设计动机的评论内容?是否有机会更新文档以减少混淆? 对 JDK 进行任何更改都很难,而且他们特别不喜欢“解释”类型的 cmets。不过我也许能找到办法。【参考方案2】:

Instant 类没有时区。它像在 UTC 区域中一样打印,因为它必须在某个区域中打印(您不希望它以刻度打印,对吗?),但这不是它的时区 - 如下例所示:

Instant instant=Instant.now();
System.out.println(instant);//prints 2014-05-14T06:18:48.649Z
System.out.println(instant.atZone(ZoneId.of("UTC")));//prints 2014-05-14T06:18:48.649Z[UTC]

ZoneOffset.fromOffsetDateTime.from 的签名接受任何时间对象,但它们对于某些类型会失败。 java.time 似乎没有用于具有时区或偏移量的时间的接口。这样的接口可以声明getOffsetgetZone。由于我们没有这样的接口,所以这些方法在多个地方分别声明。

如果您有ZonedDateTime,您可以将其称为getOffset。如果你有一个OffsetDateTime,你也可以称它为偏移量——但这是两个不同的getOffset 方法,因为这两个类没有从一个公共接口获取该方法。这意味着,如果您有一个 Temporal 对象,您必须同时测试它是否是 instanceof 才能获得偏移量。

OffsetDateTime.from 通过为您执行此操作来简化流程 - 但由于它也不能依赖通用接口,因此它必须接受任何 Temporal 对象并为那些没有偏移量的对象抛出异常.

【讨论】:

谢谢。底线是Instant 不能用作任何from 方法的有效参数。如果在每种数据类型的 API 文档中添加单行这样的效果会很好。 不只是Instance - from 对于任何不具备所有必需信息的对象都会失败。例如 - ZonedDateTime.from(Year.now()) 也失败了。在每个 from 方法的文档中列出所有不起作用的类是没有意义的 - 相反,如果无法进行转换,它们只是指定它抛出 DateTimeException 一个更正:符号 Z IS UTC 遵循 ISO-8601 文件。说“Z”和“Z[UTC]”有不同的含义是非常值得怀疑的。我个人认为后一种情况是多余的。 @MenoHochschild 在这种特定情况下,它们确实具有不同的含义。 Z 声明时间根据 UTC 时区打印,而 [UTC] 声明 Temporal 对象设置为 UTC 时区。在第一种情况下,Instant 对象实际上并不在 UTC 时区中——它只会以这种方式打印,因为这是默认设置。 当然Instant在内部表示为UTC,但这个类的想法是抽象出时区。就from 方法而言,Instant 没有可以使用的时区。【参考方案3】:

只是一个 w.r.t 转换的例子,我相信有些人会遇到异常

(java.time.DateTimeException: 无法从中获取 LocalDateTime TemporalAccessor:2014-10-24T18:22:09.800Z 类型为 java.time.Instant)

如果他们尝试 -

LocalDateTime localDateTime = LocalDateTime.from(new Date().toInstant());

要解决问题,请传入区域-

LocalDateTime localDateTime = LocalDateTime.from(new Date().toInstant().atZone(ZoneId.of("UTC")));

【讨论】:

toInstant().atZone(ZoneId.systemDefault()) 对我有用。【参考方案4】:

从 Instant 获取 LocalDateTime 的关键是提供系统的 TimeZone:

LocalDateTime.ofInstant(myDate.toInstant(), ZoneId.systemDefault())

附带说明,请注意多区域云服务器,因为时区肯定会改变。

【讨论】:

以上是关于Java SE 8 TemporalAccessor.from 与 java.time.Instant 对象一起使用时出现问题的主要内容,如果未能解决你的问题,请参考以下文章

OpenJDK 8 是 Java SE 8 的参考实现吗?

JAVA SE 8安装与配置

Java SE 8 for the Really Impatient读书笔记——Java 8 Lambda表达式

Java核心技术读书笔记12-Java SE 8引入的流

Java SE 8 的流库学习笔记

Oracle Java SE 8 发行版更新:限制商业或生产用途