Java计算1927年时间戳差值错误?时区变更惹的祸
2025-03-24 16:23:41
1927 年的时间差之谜:为什么 Java 计算出的时间戳差值不对?
碰到个奇怪的问题!一段 Java 代码解析两个日期字符串,这两个日期就差一秒钟,结果计算出来的 Unix 时间戳 (秒) 却不是差 1,而是一个意料之外的数字,真让人摸不着头脑。
先上代码,看看这段出问题的 Java 程序:
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class TimeDiffTest {
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);
}
}
按理说,str3
和 str4
两个时间只差一秒,计算结果也应该差 1 秒。 可程序一运行,输出的结果却是:353
。 怪不怪?
更有趣的是, 我把时间往后拨一秒:
String str3 = "1927-12-31 23:54:08";
String str4 = "1927-12-31 23:54:09";
这下,ld4-ld3
输出的结果就正常了,是 1
。
1. 深究原因:时区变更惹的祸
问题出在哪儿呢?答案藏在时区 里。程序运行的环境使用了 Asia/Shanghai
时区,问题就和这个时区 1927 年的一次变更有关。
通过查找时区数据库(TZ database,也叫 Olson database),我们能发现上海时区在 1927 年 12 月 31 日午夜发生了一次调整。当时的时间从 LMT
(Local Mean Time,地方平时) 向前拨了 5 分 52 秒,变成了 CST
(China Standard Time,中国标准时间)。
可以使用这个 joda-time
的库查看具体的时区变化:
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.LocalDateTime;
public class JodaTimeTest {
public static void main(String[] args) {
DateTimeZone shanghai = DateTimeZone.forID("Asia/Shanghai");
DateTime dateTime = new LocalDateTime(1927,12,31,23,54,0).toDateTime(shanghai);
System.out.println(dateTime.getMillis()/1000);
DateTime dateTime2 = dateTime.plusSeconds(10);
System.out.println(dateTime2.getMillis()/1000);
System.out.println(dateTime2.getMillis()/1000 - dateTime.getMillis()/1000 );
}
}
这段代码的输出, 两个时间戳直接的差异, 就是我们之前遇到的那个 352.
注意:我测试时, 直接求 millis 差值,并不会触发时区变更,所以我转为seconds。
更具体的证据是,你可以在时区数据库变更历史(或者 IANA 的 tzdata
)中直接看到记录。 简单来说,当时为了统一时间标准,对时钟做了拨快。
这意味着,1927-12-31 23:54:08
这个时间,实际上在 Java 的 Asia/Shanghai
时区下,根据其历史规则,需要加上一个 352 秒的时区偏移。而 1928 年以后的时间就没有这个偏移了, 因为标准时间已经推行。
2. 解决问题: 修正时间戳计算
问题的原因清楚了,解决办法就相对简单。以下列出几个常见的思路。
2.1. 考虑时区偏移 (TimeZone Offset)
最直接的方法就是把时区偏移明确考虑到计算中。 我们知道 Java 的 Date.getTime()
拿到的是相对于 UTC 时间 1970 年 1 月 1 日 00:00:00 的毫秒数。出现问题的原因,在于 SimpleDateFormat
对 parse
的实现会结合时区规则,产生和后来不同的 TimezoneOffset.
可以用 TimeZone
类来获取特定时间的时区偏移量(单位:毫秒)。然后, 通过这个偏移值对我们的时间戳做修正。
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;
public class TimeDiffFix {
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);
// 获取时区
TimeZone tz = TimeZone.getTimeZone("Asia/Shanghai");
// 获取指定日期的时区偏移量
int offset3 = tz.getOffset(sDt3.getTime());
int offset4 = tz.getOffset(sDt4.getTime());
long ld3 = (sDt3.getTime() + offset3)/ 1000;
long ld4 = (sDt4.getTime() + offset4)/ 1000;
System.out.println(ld4 - ld3);
}
}
通过显示的 getOffset
, 获取和 SimpleDateFormat
进行 parse
时相同的偏移信息。从而将两个时间拉到同一起跑线计算, 就得到了预期的 1
.
2.2. 使用 java.time
(Java 8+)
如果你的项目用的是 Java 8 或更高版本,强烈建议你用新的 java.time
包。这个包对日期和时间的处理更加清晰和强大,也更好地处理了时区问题。
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.Duration;
public class JavaTimeFix {
public static void main(String[] args) {
LocalDateTime ldt3 = LocalDateTime.parse("1927-12-31T23:54:07");
LocalDateTime ldt4 = LocalDateTime.parse("1927-12-31T23:54:08");
ZoneId shanghaiZone = ZoneId.of("Asia/Shanghai");
ZonedDateTime zdt3 = ldt3.atZone(shanghaiZone);
ZonedDateTime zdt4 = ldt4.atZone(shanghaiZone);
Duration duration = Duration.between(zdt3, zdt4);
System.out.println(duration.getSeconds()); // 输出 1
}
}
java.time
的设计更严谨,会考虑到时区的转变, 通过 ZonedDateTime
, 就能还原带时区的时间。
或者,我们可以直接计算 unix timestamp:
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.Instant;
public class JavaTimeFix2 {
public static void main(String[] args) {
LocalDateTime ldt3 = LocalDateTime.parse("1927-12-31T23:54:07");
LocalDateTime ldt4 = LocalDateTime.parse("1927-12-31T23:54:08");
ZoneId shanghaiZone = ZoneId.of("Asia/Shanghai");
ZonedDateTime zdt3 = ldt3.atZone(shanghaiZone);
ZonedDateTime zdt4 = ldt4.atZone(shanghaiZone);
//计算秒数差
System.out.println(zdt4.toEpochSecond() - zdt3.toEpochSecond());
}
}
这个例子里,直接用 toEpochSecond
,也可以规避问题。因为这里计算结果基于 zdt3/zdt4 这两个包含了时区信息的 ZonedDateTime
。
2.3. 使用 UTC
另一个处理这类问题的思路是用 UTC (协调世界时) 来存储和计算时间。UTC 是一个标准时间,不受时区影响。这样能够最大限度保证计算一致性.
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;
public class UtcFix {
public static void main(String[] args) throws ParseException {
SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
sf.setTimeZone(TimeZone.getTimeZone("UTC"));// 设置解析器使用 UTC 时区
String str3 = "1927-12-31 23:54:07";
String str4 = "1927-12-31 23:54:08";
// 这两个时间,需要根据上海时区计算得到相对于UTC的偏移量。再手动做转换.
Date sDt3 = sf.parse(str3);
Date sDt4 = sf.parse(str4);
TimeZone tz = TimeZone.getTimeZone("Asia/Shanghai");
int offset3 = tz.getOffset(sDt3.getTime());
int offset4 = tz.getOffset(sDt4.getTime());
//在 UTC 的基础上反向偏移. 如此得到UTC时间
long utc3 = (sDt3.getTime() + offset3 );
long utc4 = (sDt4.getTime() + offset4);
System.out.println( (utc4 -utc3)/1000);
}
}
安全建议: 无论选择哪种方案,都应尽量显性处理。尤其遇到历史时区可能有调整的情况时,一定在代码中写清楚如何处理时区问题。这样接手维护代码的人,才能够理解这段逻辑是基于什么规则处理的。
并且,代码一定要有良好的单元测试来覆盖.尤其要重点关注在时区发生变化的边界, 例如上面的跨1927-1928年,程序是不是依旧能够正常工作.
尽量统一存储,避免混用多种存储方式。
3. 小结
出现这种时间戳计算不准的问题,往往是由于我们忽略了时区的历史变化。这次的问题根源就在于上海时区 1927 年的那次变更。要保证计算精确,关键在于搞清楚时间处理所用的时区,并根据需求选用合适的 API 和策略。 Java 8 之后的java.time
库更推荐大家使用,它提供了一个更加强大的解决方案。如果用的是老版本的 Java, 通过理解TimeZone
和 偏移量的逻辑,也可以解决这类时区问题。