为什么减去这两个纪元毫次(1927 年)会得到一个奇怪的结果?

Why is subtracting these two epoch-milli Times (in year 1927) giving a strange result?

提问人:Freewind 提问时间:7/27/2011 最后编辑:cellepoFreewind 更新时间:11/11/2023 访问量:794365

问:

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

如果我将日期更改为 1 秒后的时间:

String str3 = "1927-12-31 23:54:08";  
String str4 = "1927-12-31 23:54:09";  

然后将是.ld4-ld31


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
Java 日期 时区

评论

125赞 Phil H 7/12/2012
真正的答案是始终使用自纪元以来的秒数进行记录,就像 Unix 纪元一样,使用 64 位整数表示(如果要允许在纪元之前使用标记,则进行签名)。任何现实世界的时间系统都有一些非线性、非单调的行为,如闰时或夏令时。
20赞 Thorbjørn Ravn Andersen 10/14/2014
关于这类事情的精彩视频:youtube.com/watch?v=-5wpm-gesOY
6赞 TRiG 7/4/2015
另一个来自同一个人,@Thorbj ørnRavnAndersen:youtube.com/watch?v=Uqjg8Kk1HXo(闰秒)。(这个来自汤姆·斯科特(Tom Scott)自己的YouTube频道,而不是来自Computerphile。
5赞 Remember Monica 9/14/2020
@Phil H“自纪元以来的秒数”(即Unix时间)也是非线性的,从某种意义上说,POSIX秒不是SI秒,并且长度不同
4赞 erickson 5/5/2022
POSIX 秒的长度可能会有所不同,但步进是一种允许的实现选项,而且并不少见。也就是说,如果添加闰秒,则相隔一秒的时间戳之间的差值可以为零,如果时间戳相差不到一秒,则相差为负数。因此,Unix 时间与其他现实世界的时间系统一样具有非单调行为,并且不是灵丹妙药。

答:

11848赞 Jon Skeet 7/27/2011 #1

这是 12 月 31 日上海的时区变更。

有关1927年在上海的详细信息,请参阅此页面。基本上在 1927 年底的午夜,时钟倒退了 5 分 52 秒。因此,“1927-12-31 23:54:08”实际上发生了两次,看起来 Java 正在将其解析为该本地日期/时间的较晚可能的时刻 - 因此存在差异。

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

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

我之所以注意到这一点,是因为我在 Noda Time 中以单元测试的形式收集了这样的问题......测试现在已经改变,但它只是表明 - 即使是历史数据也不安全。

在 TZDB 2014f 中,更改的时间已移至 1900-12-31,现在仅更改了 343 秒(因此,如果您明白我的意思,则 和 之间的时间是 344 秒)。tt+1


回答一个关于 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

1737赞 Michael Borgwardt 7/27/2011 #2

您遇到了本地时间不连续性

当当地标准时间即将到达星期日时,1.1928年1月, 00:00:00 时钟向后拨 0:05:52 小时到 31 日星期六。 1927 年 12 月 23:54:08 当地标准时间

这并不特别奇怪,而且由于政治或行政行为而改变时区,几乎在任何地方都发生过。

评论

8赞 uckelman 11/28/2020
它每年在遵守夏令时的任何地方发生两次。
1赞 iwat0qs 3/27/2022
我认为这不是夏令时。它只回来了 10 分钟,而且只有一次。同时,与 DST 相关的更改每年可能发生两次......或每年 4 次(由于斋月)。在某些设置中,甚至每年一次。没有规则:)
1赞 Henry Brice 4/11/2023
通常,这些天 DST 不会发生这种情况,因为时钟更改是在凌晨 1 点或 2 点完成的,以确保不会发生日期更改,并且日期不会重复。
743赞 Raedwald 7/28/2011 #3

这种奇怪现象的寓意是:

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

评论

15赞 Dag Ågren 11/3/2020
这些点都不会影响这个结果 - 它正好落在第三个要点下 - 而且,这是UTC被定义之前的几十年,因此不能真正有意义地用UTC来表达。
414赞 PatrickO 7/31/2011 #4

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

这样,您将能够走过小时或分钟发生两次的任何时间段。

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

347赞 Rajshri 5/16/2012 #5

您可以使用以下代码来代替转换每个日期:

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

然后看到结果是:

1
233赞 Daniel C. Sobral 1/3/2014 #6

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

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

请注意,时区数据库的 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());

然后是一秒,而将是五分五十三秒。durationAtEarlierOffsetdurationAtLaterOffset

此外,这两个偏移量是相同的:

// 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);

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

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

265赞 user1050755 2/18/2014 #7

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

两年前的 JDK 6,以及最近更新的 JDK 7 25

要吸取的教训:不惜一切代价避免使用非UTC时间,但显示除外。

评论

2赞 mjaggard 6/23/2022
也许是为了显示‽ 我不确定我是否想使用你的软件,而且我住在格林威治标准时间,这半年的时间都接近UTC。
0赞 Étienne Miret 1/11/2023
@mjaggard GMT 最多是全年 UTC 的 1 秒 ;)
0赞 mjaggard 1/17/2023
对不起,@ÉtienneMiret清楚一点,我的意思是“我住在格林威治标准时间......半年”
191赞 user1050755 11/26/2014 #8

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

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

对于 Java Community Process 成员,我建议:

  • make localized 方法,而不是默认方法,但要求用户显式请求本地化。
  • 改用为 FIXED 默认值,因为这只是今天的默认值。没有理由做其他事情,除非你想产生这样的线程。UTF-8/UTC

我的意思是,拜托,全局静态变量不是反面向对象模式吗?没有别的是由一些基本的环境变量给出的普遍默认值......

40赞 new Q Open Wid 2/11/2019 #9

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

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

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

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

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

-1赞 Samuel Marchant 10/23/2021 #10

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

评论

1赞 Anonymous 10/23/2021
原始代码将毫秒除以 1000 以转换为秒。因此,输出当然可以是 1(在大多数时区中为 1)。
0赞 Samuel Marchant 10/23/2021
奇怪的是,你应该提到时区,因为在原子钟之前没有“好的”官方时间比较器,只有使用天文学的一般协议。将原子钟之前的任何日期(绝对最好)与 java TZ(具有区域和日历系统的当前 TZ 数据库运行时更新)相关联是一场闹剧。在运行时版本数据库之前使用 java TZ 进行时间计算是一场闹剧,因为在安装 TZ 数据库 JRE 文件版本之前没有真正的分区来准确使用。请参阅 TZ 和自定义区域实现的历史数据库和取证数据库。
0赞 Mojtaba Hosseini 11/11/2023 #11

💡额外信息

这是因为夏令时,正如其他人在他们的回答中提到的那样。我想指出有关日期和日历的其他事实,以向您展示:

🧠 为什么你永远不能假设日期

除了许多现有的日历和规则(如闰年和夏令时)外,纵观历史,日历还发生了许多不遵循任何顺序的变化。

例如,在 1752 年,英格兰及其殖民地使用的日历与欧洲大多数其他地区使用的公历不同步 11 天。🤷🏻‍♂️

因此,他们通过一系列步骤对其进行了更改:

  • 1750 年 12 月 31 日之后是 1750 年 1 月 1 日(在“旧式”日历下,12 月是第 10 个月,1 月是第 11 个月)
  • 1750 年 3 月 24 日之后是 1751 年 3 月 25 日(3 月 25 日是“旧式”年的第一天)
  • 1751 年 12 月 31 日之后是 1752 年 1 月 1 日(从 3 月 25 日切换到 1 月 1 日作为一年的第一天)
  • 1752 年 9 月 2 日之后是 1752 年 9 月 14 日(与公历相差 11 天)
  • 更多信息请点击此处

另一个例子是基于月亮的日历,如回历日期,可以根据每个月的月相每年更改一次。

因此,有些日历甚至有超过10天的间隔,有些日历每年都有些不一致!在使用日历时,我们应该始终注意很多参数(如时区、区域设置、夏令时、日历标准本身等),并始终尝试使用经过测试且准确的日期系统。

⚠️ 更喜欢在线日历,因为一些日历(如伊朗回历日历)正在积极变化,而无法预测。