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

java date timezone

如果我运行以下程序,它会解析两个引用时间为 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 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秒并且长度不同

POSIX 秒的长度可能会有所不同,但步进也是一种允许且不常见的实现选项。也就是说,当添加闰秒时,相隔一秒的时间戳之间的差异可以为零,如果时间戳相隔小于一秒,则可以为负。因此,Unix 时间具有与其他现实世界时间系统一样的非单调行为,并且不是万能药。

J
Jon Skeet

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

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

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

编辑:停止新闻!历史变迁...

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

我之所以注意到这一点,是因为我在 Noda Time 以 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 假定为有效的概念),因此其他库不需要引入这种人为的转换.

M
Michael Borgwardt

您遇到了 local time discontinuity

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

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

在遵守 DST 的任何地方每年都会发生两次。

这个不是夏令时,我想。才10分钟,而且只有一次。同时,与 DST 相关的更改可能每年发生两次......或每年发生 4 次(由于斋月)。甚至在某些设置中每年一次。那里没有规则:)

R
Raedwald

这种奇怪的寓意是:

尽可能使用 UTC 中的日期和时间。

如果不能以 UTC 显示日期或时间,请始终指明时区。

如果您不能要求输入 UTC 日期/时间,则需要明确指示的时区。

这些点都不会影响这个结果 - 它完全属于第三个要点 - 而且,这是一个甚至比 UTC 定义早几十年的时间,因此不能真正有意义地用 UTC 表示。

A
Aleksandr Dubinsky

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

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

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

n
new QOpenGLWidget

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

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

然后看到结果是:

1
d
davnicwil

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

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

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

可能是为了展示‽ 我不确定是否要使用您的软件,而且我住在格林威治标准时间,这与 UTC 足够接近半年。

n
new QOpenGLWidget

正如其他人所解释的那样,那里存在时间不连续性。 Asia/Shanghai 处的 1927-12-31 23:54:08 有两个可能的时区偏移,但 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 将是 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);

您可以检查转换是否是该日期/时间存在多个有效偏移的重叠,或者该日期/时间对该区域 ID 无效的间隙 - 通过使用 isOverlap()isGap() 方法3}。

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

D
Derek Wang

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

导出 LC_ALL=C TZ=UTC

将系统时钟设置为 UTC

除非绝对必要,否则永远不要使用本地化实现(即仅用于显示)

我向 Java Community Process 成员推荐:

制作本地化方法,不是默认方法,但需要用户明确请求本地化。

使用 UTF-8/UTC 作为 FIXED 默认值,因为这只是今天的默认值。没有理由做其他事情,除非你想产生这样的线程。

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

n
new QOpenGLWidget

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

上海时间是23:54:07,按当地标准时间,但5分52秒后,转至次日00:00:00,当地标准时间又变回23:54:08。所以,这就是为什么这两次之间的差异是 343 秒,而不是 1 秒,正如您所期望的那样。

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

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

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

V
Vlad

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

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

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

S
Samuel Marchant

结果它永远不会是“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 和自定义区域实现,请参阅历史学家和取证。