为啥减去这两次(在 1927 年)会产生奇怪的结果?

Posted

技术标签:

【中文标题】为啥减去这两次(在 1927 年)会产生奇怪的结果?【英文标题】:Why is subtracting these two times (in 1927) giving a strange result?为什么减去这两次(在 1927 年)会产生奇怪的结果? 【发布时间】:2011-10-14 01:20:06 【问题描述】:

如果我运行以下程序,它会解析两个引用时间相隔 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

【问题讨论】:

真正的答案是始终,始终使用自纪元以来的秒数进行日志记录,如 Unix 纪元,使用 64 位整数表示(签名,如果你想在纪元之前允许戳记)。任何现实世界的时间系统都有一些非线性、非单调的行为,例如闰小时或夏令时。 关于这类事情的精彩视频:youtube.com/watch?v=-5wpm-gesOY 另一个来自同一个人,@ThorbjørnRavnAndersen:youtube.com/watch?v=Uqjg8Kk1HXo(闰秒)。 (这个来自 Tom Scott 自己的 YouTube 频道,不是来自 Computerphile。) @Phil H “自纪元以来的秒数”(即 Unix 时间)也是非线性的,因为 POSIX 秒不是 SI 秒并且长度不同 Unix 时间也不是单调的,如果你包括亚秒(常见于日志应用程序)。它会在闰秒内向后跳。 【参考方案1】:

12 月 31 日上海时区变化。

上海1927详情请见this page。基本上在 1927 年底的午夜,时钟倒退了 5 分 52 秒。所以“1927-12-31 23:54:08”实际上发生了两次,看起来 Java 将其解析为该本地日期/时间的 later 可能瞬间 - 因此存在差异。 p>

这只是时区经常奇怪而美妙的世界中的另一个插曲。

编辑:停止按下!历史变迁……

如果使用 TZDB 的 2013a 版本重新构建,原始问题将不再表现出完全相同的行为。在 2013a 中,结果为 358 秒,过渡时间为 23:54:03,而不是 23:54:08。

我之所以注意到这一点,是因为我在野田时间以unit tests 的形式收集此类问题... 现在测试已更改,但它只是表明 - 甚至历史数据都不安全。

编辑:历史再次改变...

在 TZDB 2014f 中,更改时间已移至 1900-12-31,现在仅更改了 343 秒(因此tt+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 认为是一个有效的概念),因此其他库不需要引入这种人为的转换.

【讨论】:

【参考方案2】:

你遇到了local time discontinuity:

当当地标准时间快到 1928 年 1 月 1 日星期日时, 00:00:00 时钟倒转 0:05:52 到 31 日星期六。 改为 1927 年 12 月 23:54:08 当地标准时间

这并不是特别奇怪,并且由于政治或行政行为导致时区切换或更改,几乎无处不在。

【讨论】:

每年在任何遵守 DST 的地方发生两次。【参考方案3】:

这种奇怪的寓意是:

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

【讨论】:

这些点都不会影响这个结果——它完全属于第三个要点——而且,这是一个比 UTC 还要早几十年的时间,因此不能真正有意义地用UTC。 虽然您的评论在技术上是正确的(最好的一种正确!)格林威治标准时间在首字母缩略词 UTC 发明之前已经存在了很长时间。【参考方案4】:

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

通过这种方式,您将能够走过小时或分钟出现两次的任何时段。

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

【讨论】:

【参考方案5】:

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

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

然后看到结果是:

1

【讨论】:

【参考方案6】:

很抱歉,时间不连续性已经移动了一点

JDK 6 两年前,JDK 7 最近在update 25。

经验教训:不惜一切代价避免非 UTC 时间,除非是为了显示。

【讨论】:

【参考方案7】:

正如其他人所解释的那样,那里存在时间不连续性。 1927-12-31 23:54:08Asia/Shanghai 有两个可能的时区偏移量,但1927-12-31 23:54:07 只有一个偏移量。因此,根据使用的偏移量,有 1 秒的差异或 5 分 53 秒的差异。

这种偏移量的轻微变化,而不是我们习惯的通常的一小时夏令时(夏令时),稍微掩盖了问题。

请注意,时区数据库的 2013a 更新将这种不连续性提前了几秒钟,但效果仍然可以观察到。

Java 8 上新的java.time 包让用户可以更清楚地看到这一点,并提供处理它的工具。给定:

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:591928-01-01 00:00:00,您会看到同样的问题,但在这种情况下,产生较长分歧的是较早的偏移量,而较早的日期有两个可能的偏移量。

解决此问题的另一种方法是检查是否正在进行转换。我们可以这样做:

// Null
ZoneOffsetTransition zot3 = shanghai.getRules().getTransition(ld3.toLocalDateTime);

// An overlap transition
ZoneOffsetTransition zot4 = shanghai.getRules().getTransition(ld3.toLocalDateTime);

您可以使用isOverlap()isGap() 来检查转换是否是该日期/时间的多个有效偏移量的重叠或该日期/时间对该区域ID 无效的间隙zot4 上的方法。

我希望这可以帮助人们在 Java 8 广泛可用后处理此类问题,或者帮助那些使用 Java 7 并采用 JSR 310 反向移植的人。

【讨论】:

【参考方案8】:

IMHO Java 中普遍存在的隐式本地化是其最大的设计缺陷。它可能是为用户界面设计的,但坦率地说,除了一些你基本上可以忽略本地化的 IDE 之外,现在谁真正将 Java 用于用户界面,因为程序员并不是它的目标受众。您可以通过以下方式修复它(尤其是在 Linux 服务器上):

导出LC_ALL=C TZ=UTC 将系统时钟设置为 UTC 除非绝对必要(即仅用于显示),否则切勿使用本地化实现

致我推荐的Java Community Process 成员:

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

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

【讨论】:

投反对票。本地化计算几乎总是正确的做法。如果您不想要它,则必须明确选择退出。【参考方案9】:

正如其他人所说,这是 1927 年的上海时变。

在上海是23:54:07,在当地标准时间,但5分52秒后,它转到第二天00:00:00,然后当地标准时间又变回23:54:08。所以,这就是为什么这两个时间之间的差异是 343 秒,而不是您预期的 1 秒。

在美国等其他地方,时间也可能会混乱。美国有夏令时。当夏令时开始时,时间向前 1 小时。但过了一会儿,夏令时结束,它倒退 1 小时回到标准时区。所以有时在比较美国的时间时,差异大约是 3600 秒而不是 1 秒。

但是这两次更改有些不同。后者不断变化,而前者只是变化。它没有变回或再次改变相同的数量。

最好使用 UTC,除非需要在显示中使用非 UTC 时间。

【讨论】:

【参考方案10】:

为避免该问题,在增加时间时,您应转换回 UTC,然后加或减。

通过这种方式,您将能够穿越小时或分钟出现两次的任何时段。

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

【讨论】:

【参考方案11】:

结果永远不会是“1”,因为 getTime() 返回长毫秒而不是秒(其中 353 毫秒是一个公平点,但 Date 的纪元始于 1970 年而不是 1920 年)。 cmmnt:您正在使用的 API 部分在很大程度上被认为已弃用。 http://windsolarhybridaustralia.x10.mx/httpoutputtools-tomcat-java.html

【讨论】:

原始代码是将毫秒除以 1000 以转换为秒。所以输出肯定可以为 1(在大多数时区为 1)。 奇怪的是你应该提到时区,因为在原子钟之前没有“好的”官方时间比较器,只有使用天文学的一般协议。将原子钟之前的任何日期(绝对最好)与 java TZ(具有针对区域和日历系统的当前 TZ 数据库运行时更新)相关联是一场闹剧。在运行时版本数据库之前使用 java TZ 进行时间计算是一场闹剧,因为在安装的 TZ 数据库 JRE 文件版本之前没有真正的分区来准确使用。有关 TZ 和自定义区域的实施,请参阅历史学家和取证。 所以你的答案是 OP 试图做的是一场闹剧?绝对是一个有效的答案。你为什么不编辑你的答案来说明这一点? 这是……公平的一点……你说,嗯,有很多原因,但核心的时间库比简单的现代时区和自定义时区和偏移更糟糕!如果您要使用 ZonedDateTime 添加或减去类方法,它们不会根据直接数字提交加法和减法,它还包含运行时的 TZ 数据库中的规则,并且可以奇怪地返回 Period 或 Duration 量化。但跌破 1970 年或日历的时代是自找麻烦。就像我说的那样,您需要警察取证研究部门或历史学家使用自己定制的 Java。

以上是关于为啥减去这两次(在 1927 年)会产生奇怪的结果?的主要内容,如果未能解决你的问题,请参考以下文章

为啥这两种将图像从 SQL CE 加载到 WPF 图像中的方法会产生不同的结果?

为啥在两次调用 promise 时 RSVP Deferred 会产生错误

为啥 spark 中的 sample 和减去方法会给出这样的结果

遍历列表并减去前面的元素会产生不需要的结果

为啥 gcc 会产生这个奇怪的程序集 vs clang?

为啥 GCC 会产生奇怪的方式来移动堆栈指针