如果我运行下面的程序,它解析两个引用时间间隔为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秒后的times:
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版本重建,原始问题将不再显示完全相同的行为。在2013年,结果将是358秒,转换时间是23:54:03而不是23:54:08。
我只注意到这一点,因为我在Noda Time收集这样的问题,以单元测试的形式…测试现在已经改变了,但它只是表明-即使是历史数据也是不安全的。
在TZDB 2014f中,更改的时间已经移动到1900-12-31,现在仅仅是343秒的更改(因此t
和t+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。1928年1月,时钟倒转了0点05:52到31日星期六。当地标准时间1927年12月23:54:08改为
这并不是特别奇怪,当时区由于政治或行政行为而切换或改变时,这种情况几乎在任何地方都发生过。
这种奇怪的寓意是:
- 尽可能使用UTC的日期和时间。
- 如果不能用UTC显示日期或时间,请务必指出时区。
- 如果您不能要求输入UTC的日期/时间,则要求明确指定时区。
当增加时间时,您应该转换回UTC,然后进行加法或减法。仅在显示时使用本地时间。
这样你就可以走过任何一个小时或分钟出现两次的时间段。
如果转换为UTC,则添加每一秒,并转换为本地时间显示。您将经历下午11:54:08 - 11:59:59的LMT,然后是下午11:54:08 - 11:59:59的CST。
不需要转换每个日期,可以使用以下代码:
long difference = (sDt4.getTime() - sDt3.getTime()) / 1000;
System.out.println(difference);
然后看到结果是:
1
很抱歉,时间不连续已经移动了一点
JDK 6是两年前的,JDK 7是最近更新的25。
经验教训:不惜一切代价避免非utc时间,可能除了显示。
正如其他人所解释的那样,这里存在时间不连续。1927-12-31 23:54:08
在Asia/Shanghai
有两个可能的时区偏移量,但1927-12-31 23:54:07
只有一个偏移量。因此,根据使用的偏移量,有1秒或5分53秒的差异。
这种偏移量的轻微变化,而不是我们习惯的一小时夏令时(夏令时间),稍微掩盖了问题。
请注意,2013年对时区数据库的更新将这种不连续性提前了几秒钟,但效果仍然是可以观察到的。
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: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);
你可以使用zot4
上的isOverlap()
和isGap()
方法来检查转换是否是一个重叠,即该日期/时间有多个有效的偏移量,还是该日期/时间对该区域id无效的间隙。
我希望这能帮助人们在Java 8广泛使用后处理这类问题,或者帮助那些采用JSR 310后端口的Java 7用户。
IMHO
Java中普遍的、隐式的本地化是其最大的设计缺陷。它可能是为用户界面设计的,但坦率地说,现在除了一些ide,谁会真正将Java用于用户界面,在这些ide中,你基本上可以忽略本地化,因为程序员并不是它的目标受众。您可以修复它(特别是在Linux服务器上):
- export
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小时。但过了一段时间,夏令时结束了,它向后一小时回到标准时区。所以有时在美国比较时间时,差异大约是3600
秒,而不是1秒。
但是这两次变化有些不同。后者不断变化,前者只是一个变化。它不会变回来,也不会再变回来。
最好使用UTC,除非需要使用非UTC时间,比如在display中。
它不能是"1";因为getTime()返回长毫秒而不是秒(其中353毫秒是一个公平的点,但是Date的epoch是从1970年开始的,而不是1920年)。注释:您正在使用的API部分在很大程度上被认为已弃用。http://windsolarhybridaustralia.x10.mx/httpoutputtools-tomcat-java.html