为什么将这两次相减(在 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

答案

这是 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 秒(因此,如果您明白我的意思,那么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认为是有效的概念)的概念,因此其他库不需要引入这种人为的转换。

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

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

这并不是特别奇怪,并且由于政治或行政行为而更改或更改了时区,一次或多次发生在各地。

这种奇怪的寓意是:

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

增加时间时,应转换回 UTC,然后加或减。仅将本地时间用于显示。

这样,您将可以度过数小时或数分钟两次的任何时期。

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

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

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

然后看到的结果是:

1

我很抱歉地说,但是时间的不连续性有所改变

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

要学习的经验:不惜一切代价避免非 UTC 时间,但可能不作展示。

正如其他人解释的那样,那里存在时间不连续性。 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将为一秒,而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);

您可以检查转型是否重叠,其中有不止一个适用于该日期 / 时间或在该日期 / 时间无效该区域 ID 间隙偏移量 - 通过使用isOverlap()isGap()的方法zot4

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

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

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

我向Java Community Process成员推荐:

  • 使本地化方法不是默认方法,但是要求用户显式请求本地化。
  • 请改用 UTF-8 / UTC 作为FIXED默认值,因为这只是今天的默认值。除了要生成这样的线程外,没有其他理由。

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

就像其他人所说的,这是 1927 年在上海的时间更改。

当上海的当地标准时间是23:54:07时,但经过 5 分 52 秒后,它转到第二天的00:00:00 ,然后当地标准时间改回了23:54:08 。因此,这就是为什么两次之间的时间差是 343 秒而不是 1 秒,这正是您所期望的。

在美国等其他地方,时间也会混乱。美国有夏令时。夏令时开始时,时间向前 1 小时。但是过了一会儿,夏令时结束了,它向后退了 1 个小时回到标准时区。因此有时在美国比较时间时,差异约为3600秒而不是 1 秒。

但是,这两个时间变化有些不同。后者不断变化,而前者只是变化。它没有变回去或再次变了相同的量。

最好在时间不改变的情况下使用 UTC,除非需要使用非 UTC 时间(如在显示中)。