为什么将这两次相减(在1927年)会得出奇怪的结果?


6824

如果我运行以下程序,该程序将解析两个日期字符串,它们分别引用间隔为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-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

23
这可能是语言环境问题。
托尔比约恩Ravn的安徒生

72
真正的答案是,自从一个纪元以来,始终要使用秒来记录日志,例如Unix纪元,具有64位整数表示形式(带符号,如果要在纪元之前允许标记)。任何现实世界的时间系统都具有某些非线性,非单调的行为,例如leap小时或夏令时。
Phil H

22
大声笑。他们在2011年为jdk6对其进行了修复。然后,两年后,他们发现它也应该在jdk7中进行了修复..从7u25开始,当然,我在发行说明中没有发现任何提示。有时我想知道Oracle修复了多少个错误,并且由于PR的原因没有告诉任何人。
user1050755

8
有关此类事情的精彩视频:youtube.com/watch?
v=-5wpm

4
@PhilH不错的是,仍然会有leap秒。因此,即使那样也不起作用。
12431234123412341234123

Answers:


10869

这是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假定这是一个有效的概念),因此其他库不需要引入这种人为的转换。


25
@Jon:出于好奇,为什么他们将时钟设置得这么“怪异”呢?大概一个小时就可以了,但是5:52分钟又是怎么回事?
约翰内斯·鲁道夫

63
@Johannes:我相信,要使其成为更普遍的全球时区,最终的偏移量为UTC + 8。巴黎在1911年做过类似的事情:timeanddate.com/worldclock/clockchange.html?n=195&year=
乔恩·斯基特

34
@Jon您是否知道Java / .NET是否可以应付1752年的9月?我总是用爱向人们展示CAL 9 1752在UNIX系统
穆斯先生

30
那么,为什么上海首先要落后5分钟呢?
Igby Largeman'7

25
@查尔斯:当时很多地方都没有那么传统的补偿。在某些国家/地区中,每个城镇都有各自的偏移量,以尽可能接近地理上正确的位置。
乔恩·斯基特

1602

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

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

这并不是特别奇怪,并且由于政治或行政行为而改变或更改了时区,一次或一次发生在几乎每个地方。


661

这种奇怪的寓意是:

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

75
转换/存储为UTC确实不能解决上述问题,因为您会遇到转换为UTC的不连续性。
Unpythonic 2011年

23
@Mark Mann:如果您的程序在内部各处都使用UTC,仅在UI中与本地时区之间进行转换,则您不会在意这种不连续性。
Raedwald,

66
@Raedwald:当然可以-1927-12-31 23:54:08的UTC时间是几点?(目前,忽略了UTC在1927年甚至不存在)。在某个时候,这个时间和日期已经进入您的系统,您必须决定如何处理它。告诉用户他们必须在UTC中输入时间只是将问题移给了用户,并不能消除问题。
尼克·巴斯汀

72
我从事大型应用程序的日期/时间重构已经有将近一年的时间了,这使我对这个线程上的活动量感到辩解。如果您正在做诸如日历之类的事情,则不能“简单地”存储UTC,因为可能呈现UTC的时区的定义会随着时间而改变。我们存储“用户意图时间”(用户的本地时间及其时区)以及用于搜索和排序的UTC,并且每当IANA数据库更新时,我们都会重新计算所有UTC时间。
taiganaut 2012年

366

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

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

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


309

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

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

然后看到的结果是:

1

72
恐怕并非如此。您可以在系统中尝试我的代码1,因为我有不同的语言环境,它将输出。
自由风

14
这仅是因为您没有在解析器输入中指定语言环境。那是糟糕的编码风格和Java的巨大设计缺陷-它固有的本地化。就个人而言,我在使用Java以避免出现这种情况的所有地方都放置了“ TZ = UTC LC_ALL = C”。另外,除非直接与用户交互并明确希望使用它,否则应避免实现的每个本地化版本。不要进行任何计算(包括本地化),除非绝对必要,否则请始终使用Locale.ROOT和UTC时区。
user1050755 2014年

226

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

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

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


27
这是不正确的。中断不是错误-只是最新版本的TZDB的数据略有不同。例如,在我使用Java 8的计算机上,如果将代码稍作更改以使用“ 1927-12-31 23:54:02”和“ 1927-12-31 23:54:03”,您仍然会看到不连续-现在是358秒,而不是353秒。TZDB的最新版本还有另一个区别-有关详细信息,请参见我的答案。这里没有真正的错误,只是关于解析日期/时间文本值的解析方式的设计决策。
乔恩·斯基特

6
真正的问题是程序员不了解本地时间与世界时间之间的转换(在任何方向上)都不是,也不是100%可靠的。对于旧的时间戳,我们所掌握的关于当地时间的数据充其量是不稳定的。对于将来的时间戳,政治行为可以更改给定本地时间映射到的通用时间。对于当前和最近的时间戳,您可能会遇到一个问题,即更新tz数据库和推出更改的过程可能比法律实施时间表要慢。
–plugwash

200

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

偏移量的这种微小变化,而不是我们通常习惯的一小时夏令时(夏季时间),使问题变得有些模糊。

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

java.timeJava 8上的新软件包使用户可以更清楚地看到这一点,并提供处理它的工具。鉴于:

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将是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:59,您会看到相同的问题1928-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的人员,这可以帮助人们处理此类问题。


1
丹尼尔,您好,我已经运行了您的代码,但是没有提供预期的输出。像durationAtEarlierOffset和durationAtLaterOffset都只有1秒,而且zot3和zot4都为null。我已经设置好复制并在我的机器上运行此代码。这里有什么需要做的吗?让我知道您是否想看一段代码。这是代码tutorialspoint.com/…您可以让我知道这里发生了什么。
vineeshchauhan

2
@vineeshchauhan这取决于Java的版本,因为tzdata中这已更改,并且不同版本的JDK捆绑了不同版本的tzdata。在我自己安装了Java,时代1900-12-31 23:54:161900-12-31 23:54:17,但是,这不工作,你共享网站上,以便他们用的是不同的Java版本比我
丹尼尔C.索布拉尔

167

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

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

我向Java Community Process成员推荐:

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

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


21

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

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

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

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.