为什么减去这两个以历元-毫秒计算的时间(在1927年)会产生奇怪的结果?

7528
如果我运行以下程序,它解析两个日期字符串,这些字符串引用相隔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(根据时间差应该是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

125
最佳做法是始终使用自纪元以来的秒数进行日志记录,例如Unix纪元,并使用64位整数表示(如果要允许纪元前的时间戳,则为带符号)。任何现实世界的时间系统都具有一些非线性、非单调的行为,例如闰秒或夏令时。 - Phil H
20
这是一个关于这类事情的很棒的视频:http://www.youtube.com/watch?v=-5wpm-gesOY。 - Thorbjørn Ravn Andersen
6
同一位作者@ThorbjørnRavnAndersen的另一个视频:https://www.youtube.com/watch?v=Uqjg8Kk1HXo (闰秒)。(这个视频来自Tom Scott自己的YouTube频道,而不是Computerphile。) - TRiG
5
“自纪元以来的秒数”(即Unix时间)也是非线性的,因为POSIX秒并非SI秒并且长度会变化。 - Remember Monica
4
POSIX 秒数的长度可能会有所不同,但是步进也是一种允许且常见的实现选项。也就是说,在添加闰秒时,时间戳相差一秒的差异可能为零,如果时间戳小于一秒,则差异可能为负。因此,Unix 时间具有类似于其他现实世界时间系统的非单调行为,并不是万能药。 - erickson
显示剩余2条评论
11个回答

11847

上海在12月31日进行了时区更改。

请参阅this page,了解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秒的微小变化(因此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假定为有效概念),因此其他库不需要引入这种人为的转换。

1737

您遇到了一个本地时间不连续的情况:(链接)

当本地标准时间即将到达1928年1月1日星期天00:00:00时,时钟被向后调整了0小时5分钟52秒,变成了1927年12月31日星期六23:54:08的本地标准时间。

这并不特别奇怪,在时区因政治或行政行动而切换或更改的各个时间点都曾经发生过类似情况。


8
每年在遵循夏令时的地方会发生两次。 - uckelman
1
这个不是夏令时,我想。它只是往回调了10分钟,而且只有一次。同时,夏令时相关的更改可能每年发生两次...或者四次(由于斋月)。在某些设置中甚至可能每年只发生一次。没有规则 :) - iwat0qs
1
一般情况下,目前夏令时的时间变更是在凌晨1点或2点进行的,以确保不会发生日期变更和重复。 - Henry Brice

743

这种奇怪现象的道理是:

  • 尽可能在使用日期和时间时使用协调世界时(UTC)。
  • 如果无法显示UTC日期或时间,请始终指明时区。
  • 如果不能要求输入的日期/时间为UTC,请要求显式表示的时区。

15
这些观点都不会影响这个结果,它完全属于第三个要点,并且此时距离UTC定义已经有几十年了,因此无法用UTC来表达其含义。 - Dag Ågren

414

在递增时间时,您应该将时间转换回协调世界时(UTC),然后进行加法或减法运算。仅在显示时使用本地时间。

这样,您就可以穿越任何发生小时或分钟重复的时段。

如果您将时间转换为UTC,然后每秒钟进行加法运算,最后再将其转换为本地时间进行显示。则您会经历晚上11:54:08 LMT - 晚上11:59:59 LMT,然后是晚上11:54:08CST - 晚上11:59:59 CST。


347

不需要将每个日期进行转换,您可以使用以下代码:

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

然后看到结果是:

1

265
抱歉要说,时间不连续性在两年前的JDK 6中移动了一点,在update 25中最近才发生在JDK 7中。需要学习的教训是:尽可能避免使用非UTC时间,除非仅用于显示。

2
也许可以显示吗?否则我不确定是否想要使用您的软件,而且我居住在GMT时区,这在一年中有一半时间足够接近UTC。 - mjaggard
@mjaggard GMT和UTC在整个年份中最多相差1秒 ;) - Étienne Miret
抱歉@ÉtienneMiret,为了明确起见,我的意思是“我居住在格林威治标准时间...一年的一半时间”。 - mjaggard

233

正如其他人所解释的那样,这里存在时间间断。在Asia/Shanghai时区中,1927-12-31 23:54:08可能有两个可能的时区偏移量,但1927-12-31 23:54:07只有一个偏移量。因此,取决于使用哪个偏移量,可能存在一秒差距或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将为1秒,而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: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变得普遍可用,或者那些使用JSR 310后移的Java 7的用户。


191

IMHO(在我看来),Java中普遍而隐含的本地化是其最大的设计缺陷。它可能是为用户界面而设计的,但老实说,除了一些可以基本忽略本地化的IDE(因为程序员不是它的目标受众)外,今天谁真正使用Java来制作用户界面呢?您可以通过以下方式解决它(特别是在Linux服务器上):

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

对于Java社区过程成员,我建议:

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

我的意思是,得了吧,全局静态变量不是反OO模式吗?除了一些原始环境变量给出的流行默认值之外,没有其他东西了......


40

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

当地标准时间在上海是 23:54:07,但在过了5分52秒后,它变成了第二天的 00:00:00,然后当地标准时间又变回到了 23:54:08。因此,这就是为什么这两个时间之间的差距是343秒,而不是你本应该预期的1秒。

时间在美国等其他地方也可能出现混乱。美国有夏令时。当夏令时开始时,时间向前推进1小时。但过一段时间后,夏令时结束,它会倒退1小时回到标准时区。因此,在比较美国的时间时,有时候差距约为3600秒,而不是1秒。

但这两次时间更改有些不同。后者是连续变化的,而前者只是一次变化。它没有以相同的幅度变回或再次变化。

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


0

额外信息

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

为什么您绝不能假设日期

除了许多现有的日历和规则,如闰年和夏令时,历史上还发生了许多不按任何顺序进行的日历变更。

例如,在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天的间隔,而且一些日历在年与年之间也存在不一致性!在使用日历时,我们应该始终注意许多参数(如时区、地区、夏令时、日历标准等),并且始终尽量使用经过测试和准确的日期系统。

⚠️ 最好选择在线日历,因为一些日历,如伊朗伊斯兰历,正在不断变化,无法预测。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接