题 为什么减去这两次(在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);
}

输出是:

353

为什么是 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

6025
2017-07-27 08:15


起源


你是否真的偶然发现了现实生活中的确切情况,或者这个问题只是为了一个益智游戏 - 只是为了它的乐趣? - Costi Ciudatu
@Costi Ciudatu:FWIW,我很容易想象这会因为减少一个更大的错误而出现 - 也就是说,“为什么这两个日期相隔一年还不到一年?” - Brooks Moses
真正的答案是始终使用秒,因为记录的纪元,如Unix纪元,具有64位整数表示(如果你想在纪元之前允许标记,则签名)。任何真实世界的时间系统都有一些非线性,非单调的行为,如闰小时或夏令时。 - Phil H
最初发布为 Oracle Bug ID 7070044 7月23日`11 - Arno
作为@Costi Ciudatu,我真的很想知道OP在哪个错误报告中挖掘了这个。我也很确定,除了作为一个谜题,这对99.9(增加9位数)百分比的用户没用。他确实获得了声誉“游戏”的奖章。 - Menelaos Bakopoulos


答案:


这是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 假设是一个有效的概念)所以其他图书馆不需要引入这种人为过渡。


9729
2017-07-27 08:31



@Gareth:不,但在那段时间检查上海时区过渡的细节是我的第一个停靠点。我最近一直致力于Noda Time的时区转换,所以模糊性的可能性几乎是我思想的最前沿...... - Jon Skeet
@Johannes:为了让它成为一个更加全球化的正常时区,我相信 - 造成 偏移量是UTC + 8。例如,巴黎在1911年做了同样的事情: timeanddate.com/worldclock/clockchange.html?n=195&year=1911 - Jon Skeet
@Charles:那时候,旅行者知道希望当地时间到处都是不同的(因为它是)。此外,手表是机械式的,并且很快就会漂移,因此我们过去每隔几天根据当地的钟表调整一次,即使他们没有旅行。那么塔钟(也漂流)是如何设定的呢?当太阳达到每日峰值时,最容易将它们设置为12:00 ...这在不同经度的每个地方都是不同的。在铁路时刻表需要某种标准化之前,这几乎无处不在。 - Michael Borgwardt
但那么:地球上的这种知识如何在这个时代幸存下来,所以近一个世纪前,它是用软件实现的?在2011年,任何向非软件工程师提及时区奇怪的人都被视为一个书呆子。 (实际上,人们希望所有的软件都能抽象出来,当他们说'中午'时,如果它是暧昧的,他们就不会给出一个该死的, 我们 软件工程师应该处理它)。但是想象一下1927年12月在上海的某个人,认为将这样的东西记下来是有意义的,并且不知何故这些信息从未丢失,删除,任何东西......心灵都被炸毁了。 - phtrivier
@EdS。它实际上只花了他15分钟,之所以显示出16分钟的差异是因为2011年7月27日由于Jon Skeet的精彩程度引起了一个小的时区差异。 - JessieArr


你遇到了一个 当地时间不连续

当地标准时间即将到达星期日,1928年1月1日,   00:00:00时钟倒退0:05:52到星期六,31。   1927年12月,当地标准时间23:54:08代替

这并不是特别奇怪,并且由于政治或行政行为导致时区被切换或改变,因此在任何时候都发生过这种情况。


1463
2017-07-27 08:38



@Jason:对于睡前阅读,我建议(现在)IANA时区数据库(以前由一个名叫Olson的可爱家伙管理,我认为)将是一个很好的资源: iana.org/time-zones。据我所知,大多数开源世界(因此提到的库)使用它作为时区数据的主要来源。 - Sune Rasmussen


这种陌生感的道德是:

  • 尽可能使用UTC中的日期和时间。
  • 如果您无法以UTC格式显示日期或时间,请始终指明时区。
  • 如果您不能要求以UTC格式输入日期/时间,则需要明确指定的时区。

580
2017-07-28 11:50



转换/存储到UTC确实无法解决所描述的问题,因为您在转换为UTC时会遇到不连续性。 - unpythonic
@Mark Mann:如果你的程序在任何地方内部使用UTC,只在UI中转换为本地时区,你不会 关心 关于这种不连续性。 - Raedwald
@Raedwald:当然可以 - 1927-12-31 23:54:08的UTC时间是多少? (暂时忽略UTC在1927年甚至不存在)。在 一些 指出这个时间和日期进入你的系统,你必须决定如何处理它。告诉用户他们必须以UTC输入时间只是将问题移交给用户,但它并没有消除它。 - Nick Bastin
我觉得这个主题上的活动量已经证明是正确的,现在已经开始对大型应用程序进行近一年的日期/时间重构。如果您正在执行类似日历的操作,则无法“简单地”存储UTC,因为可以呈现的时区的定义将随时间而变化。我们存储“用户意图时间” - 用户的本地时间及其时区 - 以及用于搜索和排序的UTC,每当IANA数据库更新时,我们都会重新计算所有UTC时间。 - taiganaut


增加时间时,您应该转换回UTC然后加或减。仅使用当地时间进行显示。

通过这种方式,您可以在几小时或几分钟发生两次的任何时段走过。

如果您转换为UTC,请添加每秒,然后转换为本地时间进行显示。你会经过晚上11:54:08。 LMT  - 下午11:59:59 LMT然后是晚上11:54:08 CST  - 下午11:59:59 CST。


320
2017-07-30 16:55





您可以使用以下代码,而不是转换每个日期

long difference = (sDt4.getTime() - sDt3.getTime()) / 1000;
System.out.println(difference);

并看到结果是:

1

265
2018-05-16 05:31



我担心情况并非如此。您可以在您的系统中尝试我的代码,它将输出 1,因为我们有不同的语言环境。 - Freewind
这是唯一的,因为您没有在解析器输入中指定区域设置。这是糟糕的编码风格和Java中的巨大设计缺陷 - 其固有的本地化。就个人而言,我把“TZ = UTC LC_ALL = C”放在我用Java避免的地方。此外,您应该避免实现的每个本地化版本,除非您直接与用户交互并明确地想要它。除非绝对必要,否则不要对任何计算(包括本地化)使用Locale.ROOT和UTC时区。 - user1050755


我很遗憾地说,但是时间的不连续性已经发生了变化

JDK 6 两年前,在 JDK 7 就在最近 更新25

需要学习的经验:不惜一切代价避免非UTC时间,可能用于显示。


188
2018-02-17 22:44



这是不正确的。不连续性不是错误 - 只是更新版本的TZDB数据略有不同。例如,在我的使用Java 8的机器上,如果您稍微更改代码以使用“1927-12-31 23:54:02”和“1927-12-31 23:54:03”,您仍会看到不连续 - 但现在是358秒,而不是353.甚至更新版本的TZDB还有另一个区别 - 详情请参阅我的答案。这里没有真正的错误,只是围绕如何解析模糊日期/时间文本值的设计决策。 - Jon Skeet
真正的问题是程序员不明白本地时间和通用时间之间的转换(在任何一个方向上)都不是,也不可能100%可靠。对于旧的时间戳,我们在当地时间的数据充其量是不稳定的。对于未来的时间戳,政治行动可以改变给定当地时间映射到的普遍时间。对于当前和最近的过去时间戳,您可能会遇到这样的问题:更新tz数据库和推出更改的过程可能比法律的实施计划慢。 - plugwash


正如其他人所解释的那样,那里有一段时间不连续。有两种可能的时区偏移 1927-12-31 23:54:08 在 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 将是5分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反向端口的Java 7的人。


168
2018-01-03 14:43



嗨丹尼尔,我已经运行了你的代码,但它没有按预期提供输出。例如durationAtEarlierOffset和durationAtLaterOffset都只有1秒,而zot3和zot4都是null。我已经设置刚刚复制并在我的机器上运行此代码。有什么需要在这里完成的。如果您想查看一段代码,请告诉我。这是代码 tutorialspoint.com/... 你能让我知道这里发生了什么。 - vineeshchauhan
@vineeshchauhan它取决于Java的版本,因为这在tzdata中已经改变,并且不同版本的JDK捆绑了不同版本的tzdata。在我自己安装的Java上,时间是 1900-12-31 23:54:16 和 1900-12-31 23:54:17,但这不适用于您共享的站点,因此他们使用的是不同于我的Java版本。 - Daniel C. Sobral


恕我直言,无处不在, 含蓄 Java中的本地化是其最大的设计缺陷。它可能适用于用户界面,但坦率地说,今天谁真正使用Java作为用户界面,除了一些IDE,你基本上可以忽略本地化,因为程序员并不完全是它的目标受众。您可以通过以下方式修复它(特别是在Linux服务器上):

  • export LC_ALL = C TZ = UTC
  • 将系统时钟设置为UTC
  • 除非绝对必要,否则永远不要使用本地化的实现

到了 Java社区流程 我推荐的成员:

  • 使本地化方法不是默认方法,但要求用户明确请求本地化。
  • 使用UTF-8 / UTC作为 固定 默认,因为这只是今天的默认值。除非您想生成这样的线程,否则没有理由做其他事情。

我的意思是,来吧,不是全局静态变量是反OO模式吗?没有其他东西是由一些基本环境变量给出的普遍违约.......


137
2017-11-26 15:58