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 ZoneId
的 ZonedDateTime
ZonedDateTime.from
的文档在我阅读时似乎支持这一点。
事实上,ZoneOffset.from(anInstant)
以 DateTimeException
失败,我想出于这个原因,OffsetDateTime.from(anInstant)
也失败了,其他两个也是如此。
这是预期的行为吗?
【问题讨论】:
【参考方案1】:简答:
JSR-310 设计者不希望人们通过 ZoneId
、ZoneOffset
、OffsetDateTime
、ZonedDateTime
等类型的静态 from() 方法在机器时间和人类时间之间进行转换。如果您仔细研究 javadoc,则会明确指定这一点。而是使用:
OffsetDateTime#toInstant():Instant
ZonedDateTime#toInstant():Instant
Instant#atOffset(ZoneOffset):OffsetDateTime
Instant#atZone(ZoneId):ZonedDateTime
静态 from() 方法的问题在于,否则人们可以在 Instant
和 LocalDateTime
之间进行转换,而无需考虑时区。
长答案:
无论将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
有趣的是这里缺少什么,最重要的是缺少一个与区域相关的字段。 作为一个原因,我们经常听到像Instant
或java.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
获得 ZoneId
和 ZoneOffset
。异常消息内容如下:
“无法从 TemporalAccessor 获取 ZoneId:1970-01-01T00:00:00Z of type java.time.Instant”
最后,我们看到所有静态 from() 方法都无法在人类时间和机器时间之间进行转换,即使这看起来很直观。在某些情况下,假设LocalDate
和Instant
之间的转换是有问题的。指定了这种行为,但我预测您的问题不会是此类问题的最后一个问题,许多用户将继续感到困惑。
我认为真正的设计问题是:
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
方法的参数支持 TemporalAccessor
objects 进行了编程分析。 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.from
和 OffsetDateTime.from
的签名接受任何时间对象,但它们对于某些类型会失败。 java.time
似乎没有用于具有时区或偏移量的时间的接口。这样的接口可以声明getOffset
和getZone
。由于我们没有这样的接口,所以这些方法在多个地方分别声明。
如果您有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 对象一起使用时出现问题的主要内容,如果未能解决你的问题,请参考以下文章