为什么减去这两次(在1927年)给出一个奇怪的结果?
如果我运行以下程序,它解析引用时间间隔为1秒的两个日期字符串并比较它们:
public static void main(String[] args) throws ParseException {
SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String str3 = "1927-12-31 23:54:07";
String str4 = "1927-12-31 23:54:08";
Date sDt3 = sf.parse(str3);
Date sDt4 = sf.parse(str4);
long ld3 = sDt3.getTime() /1000;
long ld4 = sDt4.getTime() /1000;
System.out.println(ld4-ld3);
}
输出是:
为什么ld4-ld3
不1
(正如我所期望的那样,在时间上只有一秒钟的差异),但是353
?
如果我将日期更改为1秒后的时间:
String str3 = "1927-12-31 23:54:08";
String str4 = "1927-12-31 23:54:09";
然后ld4-ld3
会1
.
Java版本:
java version "1.6.0_22"
Java(TM) SE Runtime Environment (build 1.6.0_22-b04)
Dynamic Code Evolution Client VM (build 0.2-b02-internal, 19.0-b04-internal, mixed mode)
Timezone(`TimeZone.getDefault()`):
sun.util.calendar.ZoneInfo[id="Asia/Shanghai",
offset=28800000,dstSavings=0,
useDaylight=false,
transitions=19,
lastRule=null]
Locale(Locale.getDefault()): zh_CN
回答
这是12月31日在上海的时区变化.
有关1927年在上海的详细信息,请参阅此页.基本上在1927年底的午夜,时钟倒流了5分52秒.因此"1927-12-31 23:54:08"实际上发生了两次,看起来Java正在将其解析为当地日期/时间的后续可能时刻 - 因此存在差异.
只是在经常奇怪和奇妙的时区世界的另一集.
编辑:停止按!历史变迁......
原来的问题将不再表现出不尽相同的行为,如果与重建版本2013a TZDB.在2013a中,结果为358秒,转换时间为23:54:03而不是23:54:08.
我只是注意到了这一点,因为我正在Noda Time中以单元测试的形式收集这样的问题......测试现在已经改变了,但它只是显示 - 甚至历史数据都不安全.
编辑:历史再次发生变化......
在TZDB 2014f中,更改的时间已经变为1900-12-31,现在只有343秒的变化(因此,如果您看到我的意思,则介于t
和之间的时间t+1
为344秒).
编辑:回答关于1900年转换的问题......看起来Java时区实现将所有时区视为在1900 UTC开始之前的任何时刻的标准时间内:
import java.util.TimeZone;
public class Test {
public static void main(String[] args) throws Exception {
long startOf1900Utc = -2208988800000L;
for (String id : TimeZone.getAvailableIDs()) {
TimeZone zone = TimeZone.getTimeZone(id);
if (zone.getRawOffset() != zone.getOffset(startOf1900Utc - 1)) {
System.out.println(id);
}
}
}
}
上面的代码在我的Windows机器上没有输出.因此,任何在1900年开始时具有除标准偏移之外的任何偏移的时区都将其视为过渡.TZDB本身有一些数据早于此,并且不依赖于"固定"标准时间的任何想法(getRawOffset
假设这是一个有效的概念),因此其他库不需要引入这种人工转换.
- @Gareth:不,但在那段时间检查上海时区过渡的细节是我的第一个停靠点.我最近一直致力于Noda Time的时区转换,所以模糊性的可能性几乎是我思想的最前沿......
- 但那么:地球上的这种知识如何在这个时代幸存下来,所以近一个世纪前,它是用软件实现的?在2011年,任何向非软件工程师提及时区奇怪的人都被视为书呆子.(实际上,人们希望所有的软件都能抽象出来,如果它们是暧昧的,当他们说'中午'时,他们就不会给出一个该死的,*我们*软件工程师应该处理它).但是想象一下1927年12月在上海的某个人,认为将这样的东西记下来是有意义的,并且不知何故这些信息从未丢失,删除,任何东西......心灵被炸毁了.
- @Charles:当时,旅行者知道希望当地时间到处都是不同的(因为它是).此外,手表机械和快速漂移,所以我们过去每隔几天根据当地的钟表调整它们,即使他们没有旅行.那么塔钟(也漂流)是如何设定的呢?当太阳达到每日高峰时,最容易将它们设置为12:00 ...这在不同经度的每个地方都是不同的.在铁路时刻表需要某种标准化之前,这几乎无处不在.
- @EdS.它实际上只花了他15分钟,之所以显示出16分钟的差异是因为2011年7月27日由于Jon Skeet是多么出色而导致的时区差异很小.
- @Johannes:为了使它成为一个更全局正常的时区,我相信 - *结果*偏移是UTC + 8.巴黎在1911年做过同样的事情,例如:http://www.timeanddate.com/worldclock/clockchange.html?n = 195&year = 1911
- @Jon你是否碰巧知道Java/.NET是否应对1752年的9月?我一直喜欢在unix系统上展示人们9 952
- 那么为什么上海首先出现了5分钟?
- @Charles:当时很多地方的常规抵消都较少.在一些国家,不同的城镇各自有自己的偏移量,尽可能接近地理上的正确.
- @Jon:出于好奇,为什么他们将时钟设置为这样一个"怪异"的间隔?像一个小时的任何事情似乎都是合乎逻辑的,但是为什么它是5:52分钟呢?
- @yahelc,详细说明:时区变化基本上是一小组不同的可能参数化事件.代码实现了这组事件,时区数据库提供了每个时区的事件和参数列表.顺便说一下,这个时区数据库可以独立于JRE本身进行更新:http://www.oracle.com/technetwork/java/javase/tzupdater-readme-136440.html
- 我对2011年7月27日8:15(问题的日期时间)和2011年7月27日8:31(问题的回答日期时间)之间的时间更感兴趣..是Jon Skeet知道这个答案需要花多长时间才能有16分钟的差异并解释一下?这不应该更长吗?或者我是否真的低估了Jon Skeet提出的这个问题?
- 这是否意味着,在Java日期库中的某个地方,处理这个非常边缘的情况(以及其他所有奇怪的边缘情况)的逻辑?
- @yahelc:没有 - 只有一般的时区代码,它处理各种历史时区转换.
- 我现在明白为什么汤姆斯科特说不要这样做.https://www.youtube.com/watch?v=-5wpm-gesOY
- @sksallaj:如果你看一下第一个答案,那就很短了.如果您认为可能知道在哪里查找数据,则需要很长时间才能找到数据...
- @GopsAB:我不会说"11:54:08实际上是00:00:00",但是第二次出现的11:54:08就是00:00:00,如果UTC偏移已经'改变了.这实际上不是DST的例子,顺便说一句.DST不是要改变夜间/白天的持续时间 - 它只是试图让"典型的工作时间"在更多的白天时间内发生.无论如何,我认为这里的讨论真的很充分......
- @Itme ... Alex:好吧``java.util.Calendar`和`java.util.Date`肯定在很多方面都很差.在Noda Time中,当您将`LocalDateTime`转换为`ZonedDateTime`时,您必须指示您希望如何处理模糊或跳过的瞬间.
- @Mr Moose:我相信Joda Time将根据您选择的日历执行不同的操作,Noda Time将在我们实施切换日历时执行.我不知道"本机"Java/.NET类.
- 是否有完整的列表列出了历史上所有这些时间的修正?
- @ KevinH.Lin:IANA的时区数据是:http://iana.org/timezones
- @DanTemple:答案并不正确 - 时区数据库稍微改变了转换时间,但是如果你适当地编辑原始程序,你仍会看到不连续性.
- @GopsAB:是的,但我认为这并不意味着你的意思."相同时区"并不意味着"相同的UTC偏移".例如,我在时区被识别为"欧洲/伦敦".每年一次,我们的当地时间会向前跳一小时.每年一次,我们的当地时间减少一个小时.在这两种情况下,两个相隔一秒的通用时间最终具有*局部*时间,远远超过一秒.
- 所以,时钟在上海5:52回归.所以11:54:08实际上是00:00:00.所以区分11:54:07从11:54:08实际上就像00:00:00 - 11:54:07.我对吗?而节日实际上节省的时间实际上是减少时钟以增加日期并减少以增加夜间持续时间.谢谢
- @Itme ... Alex:不,这里只有一个时区 - 有两个不同的UTC偏移.一个更好的API将允许您确定在解析模糊的本地时间时您的意思,当然:)
- @GopsAB:对于*local*次而言并非如此,因为本地时间经常前进或后退,通常是由于DST.再次 - 你是否生活在夏令时的文化中?那么"两次都会改变"是什么意思?目前还不清楚是什么令你感到困惑.
- 我理解它背后的原因,以及它为什么这样实现.我个人认为结果应该是"未确定的".它现在实施的方式就好像第一次出现的23:54:08-23:59:59从未发生过.1和353都是正确的答案.从技术上讲,减去2个不同的时区,感觉不正确的行为.
- @Jon Skeet:好的,两种不同的UTC偏移:-) ......但是你理解我的意思.java(以及很可能是其他库)中的实现有点差
- @JonSkeet如果[此答案](http://stackoverflow.com/a/21840785/2508646)是正确的,那么将其也添加到您的答案中可能是明智的,因为这样超级最新
- 汤姆斯科特还有一个关于闰秒的视频@ BenC.R.Leggiero:https://www.youtube.com/watch?v = Uqjg8Kk1HXo.(这个来自汤姆斯科特自己的YouTube频道,而不是来自Computerphile.)
- @GopsAB:"来自同一时区的两次"是什么意思?暂时忽略上海 - 你是否住在一个观察夏令时的时区?如果是的话,它什么时候改变?这是一个很好的方式来看到当地时间变化与UTC不同的数量......]
您遇到了当地时间不连续:
这并不是特别奇怪,并且由于政治或行政行为导致时区被切换或改变,因此在任何时候都发生过这种情况.
- @Jason:对于睡前阅读,我建议(现在)IANA时区数据库(以前由一个名叫Olson的可爱人管理,我认为)将是一个很好的资源:http://www.iana.org/time-区域.据我所知,大多数开源世界(因此提到的库)使用它作为时区数据的主要来源.
- It happens twice a year anywhere that observes DST.
这种陌生感的道德是:
- 尽可能使用UTC中的日期和时间.
- 如果您无法以UTC格式显示日期或时间,请始终指明时区.
- 如果您不能要求以UTC格式输入日期/时间,则需要明确指定的时区.
- 转换/存储到UTC确实无法解决所描述的问题,因为在转换为UTC时会遇到不连续性.
- 我觉得这个主题上的活动数量已经证明是正确的,现在已经在大型应用程序上进行了近一年的日期/时间重构.如果您正在执行类似日历的操作,则无法"简单地"存储UTC,因为可能呈现的时区的定义将随时间而变化.我们存储"用户意图时间" - 用户的本地时间及其时区 - 以及用于搜索和排序的UTC,每当IANA数据库更新时,我们都会重新计算所有UTC时间.
- @Raedwald:当然可以 - 1927-12-31 23:54:08的UTC时间是多少?(暂时忽略UTC在1927年甚至不存在).在某些*点,此时间和日期将进入您的系统,您必须决定如何处理它.告诉用户他们必须以UTC输入时间只是将问题转移给用户,但它并没有消除它.
- @Mark Mann:如果你的程序在任何地方内部使用UTC,只在UI中转换为本地时区,那么你就不会关心这种不连续性.
- None of these points would affect this result - it falls squarely under the third bullet point - and moreover, this is a time several decades before UTC was even defined, and thus can not really meaningfully be expressed in UTC.
在递增时间时,您应该转换回UTC然后加或减.仅使用当地时间进行显示.
通过这种方式,您可以在几小时或几分钟发生两次的任何时段走过.
如果转换为UTC,请添加每秒,然后转换为本地时间进行显示.你会经历下午11时54分08秒LMT -下午11:59:59 LMT然后下午11时54分08秒CST -下午11:59:59 CST.
您可以使用以下代码,而不是转换每个日期
long difference = (sDt4.getTime() - sDt3.getTime()) / 1000;
System.out.println(difference);
并看到结果是:
1
- 我担心情况并非如此.您可以在系统中尝试我的代码,它会输出"1",因为我们有不同的语言环境.
- 这是唯一的,因为您没有在解析器输入中指定区域设置.这是糟糕的编码风格和Java中的巨大设计缺陷 - 其固有的本地化.就个人而言,我把"TZ = UTC LC_ALL = C"放在我用Java避免的地方.此外,您应该避免实现的每个本地化版本,除非您直接与用户交互并明确地想要它.除非绝对必要,否则不要对任何计算(包括本地化)使用Locale.ROOT和UTC时区.
我很遗憾地说,但是时间的不连续性已经发生了变化
两年前的JDK 6,以及最近在更新25中的JDK 7.
要学习的课程:不惜一切代价避免非UTC时间,除了显示.
- 这是不正确的.不连续性不是一个错误 - 只是更新版本的TZDB的数据略有不同.例如,在我的使用Java 8的机器上,如果您稍微更改代码以使用"1927-12-31 23:54:02"和"1927-12-31 23:54:03",您仍会看到不连续 - 但现在是358秒,而不是353.甚至更新版本的TZDB还有另一个区别 - 详情请参阅我的答案.这里没有真正的错误,只是围绕如何解析模糊日期/时间文本值的设计决策.
- 真正的问题是程序员不明白本地时间和通用时间之间的转换(在任何一个方向上)都不是100%可靠的.对于旧的时间戳,我们在当地时间的数据充其量是不稳定的.对于未来的时间戳,政治行动可以改变给定当地时间映射到的普遍时间.对于当前和最近的过去时间戳,您可能会遇到这样的问题:更新tz数据库并推出更改的过程可能比法律的实施计划慢.
正如其他人所解释的那样,那里有一段时间不连续.1927-12-31 23:54:08
at 有两种可能的时区偏移Asia/Shanghai
,但只有一种偏移1927-12-31 23:54:07
.因此,根据使用的偏移量,存在一秒差异或5分53秒差异.
这种轻微的偏移转移,而不是我们习惯的通常的一小时夏令时(夏令时),有点模糊了问题.
请注意,时区数据库的2013a更新在几秒钟之前移动了此不连续性,但效果仍然可以观察到.
java.time
Java 8上的新包使用更清楚,并提供处理它的工具.鉴于:
DateTimeFormatterBuilder dtfb = new DateTimeFormatterBuilder();
dtfb.append(DateTimeFormatter.ISO_LOCAL_DATE);
dtfb.appendLiteral(' ');
dtfb.append(DateTimeFormatter.ISO_LOCAL_TIME);
DateTimeFormatter dtf = dtfb.toFormatter();
ZoneId shanghai = ZoneId.of("Asia/Shanghai");
String str3 = "1927-12-31 23:54:07";
String str4 = "1927-12-31 23:54:08";
ZonedDateTime zdt3 = LocalDateTime.parse(str3, dtf).atZone(shanghai);
ZonedDateTime zdt4 = LocalDateTime.parse(str4, dtf).atZone(shanghai);
Duration durationAtEarlierOffset = Duration.between(zdt3.withEarlierOffsetAtOverlap(), zdt4.withEarlierOffsetAtOverlap());
Duration durationAtLaterOffset = Duration.between(zdt3.withLaterOffsetAtOverlap(), zdt4.withLaterOffsetAtOverlap());
然后durationAtEarlierOffset
将是一秒钟,而durationAtLaterOffset
将是五分53秒.
此外,这两个偏移是相同的:
// Both have offsets +08:05:52
ZoneOffset zo3Earlier = zdt3.withEarlierOffsetAtOverlap().getOffset();
ZoneOffset zo3Later = zdt3.withLaterOffsetAtOverlap().getOffset();
但这两者是不同的:
// +08:05:52
ZoneOffset zo4Earlier = zdt4.withEarlierOffsetAtOverlap().getOffset();
// +08:00
ZoneOffset zo4Later = zdt4.withLaterOffsetAtOverlap().getOffset();
你可以看到同样的问题比较1927-12-31 23:59:59
有1928-01-01 00:00:00
,不过,在这种情况下,它是产生较长发散较早的偏移量,它是有两种可能的偏移较早的日期.
另一种方法是检查是否正在进行转换.我们可以这样做:
// Null
ZoneOffsetTransition zot3 = shanghai.getRules().getTransition(ld3.toLocalDateTime);
// An overlap transition
ZoneOffsetTransition zot4 = shanghai.getRules().getTransition(ld3.toLocalDateTime);
您可以检查转换是否是重叠 - 在这种情况下,该日期/时间有多个有效偏移量 - 或间隙 - 在这种情况下,日期/时间对于该区域ID无效 - 通过使用isOverlap()
和isGap()
方法zot4
.
我希望这有助于人们在Java 8广泛使用后处理这类问题,或者使用采用JSR 310 backport的Java 7的人.
- @vineeshchauhan它取决于Java的版本,因为这在tzdata中已经改变,并且不同版本的JDK捆绑了不同版本的tzdata.在我自己安装的Java上,时间是'1900-12-31 23:54:16`和`1900-12-31 23:54:17`,但这在你分享的网站上不起作用,所以它们是使用与I不同的Java版本
恕我直言,Java中普遍存在的隐式本地化是其最大的单一设计缺陷.它可能适用于用户界面,但坦率地说,现在谁真正使用Java作为用户界面,除了一些IDE,你基本上可以忽略本地化,因为程序员并不是它的目标受众.您可以通过以下方式修复它(特别是在Linux服务器上):
- export LC_ALL = C TZ = UTC
- 将系统时钟设置为UTC
- 除非绝对必要,否则永远不要使用本地化的实现
对于Java Community Process成员,我建议:
- 使本地化方法不是默认方法,但要求用户明确请求本地化.
- 使用UTF-8/UTC作为FIXED默认值,因为这只是今天的默认值.除非您想生成这样的线程,否则没有理由做其他事情.
我的意思是,来吧,不是全局静态变量是反OO模式吗?没有其他东西是由一些基本的环境变量给出的那些普遍的默认值.......
就像其他人所说的,这是1927年在上海的时间更改。
基本上,23:54:07
上海时间是当地的标准时间,过了一会儿又转到了第二天的时间00:00:00
。但随后又切换回23:54:08
当地标准时间。因此,这就是为什么时差为343秒而不是1秒的原因。
这也可能与美国等其他地方搞混。在美国,他们有夏令时。夏令时开始时,时间会提前1个小时。但是过了一会儿,夏令时结束了,它向后退了1个小时回到标准时区。因此,有时在比较美国的时间时,差异大约是3600
几秒钟而不是1秒。
最好在时间不改变的情况下使用UTC,除非需要使用非UTC时间(如在显示中)。