关于Java:为什么减去这两次(在1927)给出一个奇怪的结果?

Why is subtracting these two times (in 1927) giving a strange result?

如果我运行以下程序,它解析两个相隔1秒引用的日期字符串,并对它们进行比较:

1
2
3
4
5
6
7
8
9
10
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秒后将日期更改为次:

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

那么,ld4-ld3就是1

Java版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
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时间以单元测试的形式收集这样的问题…测试现在已经被更改了,但它只是用来表明——即使是历史数据也不安全。

编辑:历史记录再次更改…

在TZDB 2014F中,更改的时间已经移动到1900-12-31,现在只剩下343秒的更改(所以,如果你明白我的意思,从tt+1)之间的时间是344秒)。

编辑:回答1900年过渡期的问题…看起来,Java时区实现在1900 UTC开始之前的任何时刻都将所有时区简单地置于其标准时间内:

1
2
3
4
5
6
7
8
9
10
11
12
13
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假设的有效概念),因此其他库不需要引入这种人工转换。


您遇到了本地时间中断:

When local standard time was about to reach Sunday, 1. January 1928,
00:00:00 clocks were turned backward 0:05:52 hours to Saturday, 31.
December 1927, 23:54:08 local standard time instead

这并不特别奇怪,而且随着时区因政治或行政行为而被切换或更改,几乎到处都发生过这种情况。


这种陌生感的寓意是:

  • 尽可能使用UTC格式的日期和时间。
  • 如果不能以UTC格式显示日期或时间,请始终指示时区。
  • 如果您不能要求输入日期/时间(以UTC为单位),则需要一个明确指示的时区。


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

这样,您就可以在任何时间段内行走,时间或分钟发生两次。

如果转换为UTC,请每秒添加一次,然后转换为本地时间进行显示。你将经历11:54:08下午lmt-11:59:59下午lmt,然后11:54:08下午cst-11:59:59下午cst。


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

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

结果是:

1
1


很抱歉,时间上的不连续已经有了一点变化

两年前的JDK 6,最近的JDK 7更新了25。

要吸取的教训:不惜一切代价避免非UTC时间,除了可能的显示时间。


正如其他人所解释的,那里有一个时间间断。在Asia/Shanghai处,1927-12-31 23:54:08有两个可能的时区偏移,但在1927-12-31 23:54:07处只有一个偏移。所以,根据使用的偏移量,要么有一秒的差异,要么有5分53秒的差异。

这种偏移量的轻微变化,而不是我们习惯的通常的一小时日光节约(夏季时间),稍微掩盖了这个问题。

请注意,2013年时区数据库的更新在几秒钟前就移动了这一不连续点,但效果仍然可以观察到。

Java 8上的新EDCOX1 3个包让我们更清楚地看到这一点,并提供工具来处理它。鉴于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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秒。

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

1
2
3
// Both have offsets +08:05:52
ZoneOffset zo3Earlier = zdt3.withEarlierOffsetAtOverlap().getOffset();
ZoneOffset zo3Later = zdt3.withLaterOffsetAtOverlap().getOffset();

但这两个不同:

1
2
3
4
5
// +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时,您可以看到相同的问题,不过,在这种情况下,产生较长差异的是较早的偏移量,而有两个可能的偏移量的是较早的日期。

另一种方法是检查是否正在进行转换。我们可以这样做:

1
2
3
4
5
// Null
ZoneOffsetTransition zot3 = shanghai.getRules().getTransition(ld3.toLocalDateTime);

// An overlap transition
ZoneOffsetTransition zot4 = shanghai.getRules().getTransition(ld3.toLocalDateTime);

您可以使用isOverlap()isGap()方法检查转换是否是重叠(在这种情况下,该日期/时间有多个有效偏移)或间隙(在这种情况下,该日期/时间对该区域ID无效)。

我希望这有助于人们在Java 8变得广泛可用时,或使用Java 7的JSR 310后端的人来处理这类问题。


在Java中普遍存在的隐式本地化是其最大的设计缺陷。它可能是针对用户界面的,但坦率地说,除了某些IDE,基本上可以使用Java来代替用户界面,因为基本上程序员不完全是它的目标受众。您可以通过以下方式修复它(尤其是在Linux服务器上):

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

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

  • 使本地化方法不是默认方法,而是要求用户显式请求本地化。
  • 改为使用utf-8/utc作为固定默认值,因为这只是今天的默认值。除了要生成这样的线程外,没有理由执行其他操作。

我是说,拜托,全局静态变量不是反OO模式吗?其他的都不是由一些基本的环境变量给出的普遍的默认值……


正如其他人提到的,1927年上海的时间发生了变化。

基本上说,江户十一〔11〕在上海,当地的标准时间,过了一会儿,就转到第二天的江户十一〔12〕了。但后来它又回到了23:54:08的地方标准时间。所以,这就是为什么差异是343秒而不是1秒。

这也会让其他地方如美国陷入困境。在美国,他们有夏令时。当夏令时开始时,时间向前移动1小时。但过了一段时间,夏时制结束后,它会倒退1小时回到标准时区。因此,有时在比较美国的时间时,差异大约是3600秒,而不是1秒。

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