有 Java 编程相关的问题?

你可以在下面搜索框中键入要查询的问题!

Java中处理日期和时间的不一致性

我注意到java中日期和时间的奇怪行为。我有以下代码:

public class TestDateTime {
    public static void main(String[] args) {
        TimeZone.setDefault(TimeZone.getTimeZone("Europe/Helsinki"));

        Calendar calendar = GregorianCalendar.getInstance();

        assert(calendar.getTimeZone().equals(TimeZone.getDefault()));
        //Set 1899-12-30T23:00:00
        calendar.set(1899,11,30,23,0,0);
        calendar.set(Calendar.MILLISECOND,0);

        long timeInMillis = calendar.getTimeInMillis();
        java.util.Date calendarDateTime = new java.util.Date(timeInMillis);

        LocalDateTime localDateTime = LocalDateTime.ofInstant(ofEpochMilli(timeInMillis), ZoneId.systemDefault());
        System.out.println("Time in millis: " + timeInMillis);
        System.out.println("Date: " + calendarDateTime.toString());
        System.out.println("Local DateTime: " + localDateTime.toString());
    }
}

输出为:

Time in millis: -2209086000000
Date: Sat Dec 30 23:00:00 EET 1899
Local DateTime: 1899-12-30T22:39:49

timeInMillis必须包含从1970-01-01T00:00:00Z传递的毫秒数。 Date类的实例存储从1970-01-01T00:00:00Z传递的毫秒数。 Date.toString()方法返回默认时区的本地日期和时间

因此Date.toString()LocalDateTime.toString()必须返回相同的日期和时间,但我们看到了差异(超过20分钟)

这是java的一个bug,还是我在java中错误地使用了日期和时间


共 (4) 个答案

  1. # 1 楼答案

    正如其他人指出的,区别在于Date对象没有考虑LMT(local mean time)值。这在{a2}之前已经讨论过,关于{a3}——Java8的{a4}的前身

    此外,the Joda-Time FAQ还说:

    Why is the offset for a time-zone different to the JDK?

    There are two main reasons for this.

    The first reason is that both the JDK and Joda-Time have time-zone data files. It is important to keep both up to date and in sync if you want to compare the offset between the two.

    The second reason affects date-times before the modern time-zone system was introduced. The time-zone data is obtained from the time-zone database. The database contains information on "Local Mean Time" (LMT) which is the local time that would have been observed at the location following the Sun's movements.

    Joda-Time uses the LMT information for all times prior to the first time-zone offset being chosen in a location. By contrast, the JDK ignores the LMT information. As such, the time-zone offset returned by the JDK and Joda-Time are different for date-times before the modern time-zone system.

    最后一部分(我用粗体显示)与Joda Time和Java 8有关,尽管Java 8有一组时区数据文件(与Joda Time不同)

  2. # 2 楼答案

    LocalDateTime是正确的。根据TZ数据库,该日期的GMT偏移量为^{

    # Zone  NAME        GMTOFF  RULES   FORMAT  [UNTIL]
    Zone    Europe/Helsinki 1:39:49 -   LMT 1878 May 31
                1:39:49 -   HMT 1921 May    # Helsinki Mean Time
                2:00    Finland EE%sT   1983
                2:00    EU  EE%sT
    

    历史时区极其复杂,在标准化之前,偏移量是从基于平均太阳正午之类的设置继承而来的。当追溯到那么远的时候,几乎任何偏移都是可能的,而IANA TZ database是历史数据的主要参考

    从我在数据库中看到的情况来看,直到1921年HMT被EE(S)T取代,奇怪的偏移量才被标准化为2:00:00

  3. # 3 楼答案

    这是芬兰时间变化造成的一种奇怪现象,见Clock Changes in Helsinki, Finland (Helsingfors) in 1921

    May 1, 1921 - Time Zone Change (HMT → EET)

    When local standard time was about to reach Sunday, May 1, 1921, 12:00:00 midnight clocks were turned forward 0:20:11 hours to Sunday, May 1, 1921, 12:20:11 am local standard time instead

    这20分11秒似乎就是你所观察到的

    作为Jim Garrison said in his answerLocalDateTime正确地处理了这个问题,而Calendar则没有

    实际上,旧的TimeZone似乎得到了错误的偏移量,而新的ZoneId得到了正确的偏移量,如以下测试代码所示:

    public static void main(String[] args) {
        compare(1800, 1, 1,  0, 0, 0);
        compare(1899,12,31, 23,59,59);
        compare(1900, 1, 1,  0, 0, 0);
        compare(1900,12,30, 23, 0, 0);
        compare(1921, 4,30,  0, 0, 0);
        compare(1921, 5, 1,  0, 0, 0);
        compare(1921, 5, 2,  0, 0, 0);
    }
    private static void compare(int year, int month, int day, int hour, int minute, int second) {
        Calendar calendar = new GregorianCalendar();
        calendar.clear();
        calendar.setTimeZone(TimeZone.getTimeZone("Europe/Helsinki"));
        calendar.set(year, month-1, day, hour, minute, second);
        Date date = calendar.getTime();
        
        ZonedDateTime zdt = ZonedDateTime.of(year, month, day, hour, minute, second, 0, ZoneId.of("Europe/Helsinki"));
        
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z XXX");
        sdf.setTimeZone(TimeZone.getTimeZone("Europe/Helsinki"));
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss z XXX");
        
        System.out.printf("%04d-%02d-%02d %02d:%02d:%02d   %s = %d   %s = %d   %d%n",
                          year, month, day, hour, minute, second,
                          sdf.format(date), date.getTime(),
                          dtf.format(zdt), zdt.toInstant().toEpochMilli(),
                          date.getTime() - zdt.toInstant().toEpochMilli());
    }
    

    输出

    1800-01-01 00:00:00   1800-01-01 00:00:00 EET +02:00 = -5364669600000   1800-01-01 00:00:00 EET +01:39 = -5364668389000   -1211000
    1899-12-31 23:59:59   1899-12-31 23:59:59 EET +02:00 = -2208996001000   1899-12-31 23:59:59 EET +01:39 = -2208994790000   -1211000
    1900-01-01 00:00:00   1900-01-01 00:00:00 EET +02:00 = -2208996000000   1900-01-01 00:00:00 EET +01:39 = -2208994789000   -1211000
    1900-12-30 23:00:00   1900-12-30 23:00:00 EET +01:39 = -2177548789000   1900-12-30 23:00:00 EET +01:39 = -2177548789000   0
    1921-04-30 00:00:00   1921-04-30 00:00:00 EET +01:39 = -1536025189000   1921-04-30 00:00:00 EET +01:39 = -1536025189000   0
    1921-05-01 00:00:00   1921-05-01 00:20:11 EET +02:00 = -1535938789000   1921-05-01 00:20:11 EET +02:00 = -1535938789000   0
    1921-05-02 00:00:00   1921-05-02 00:00:00 EET +02:00 = -1535853600000   1921-05-02 00:00:00 EET +02:00 = -1535853600000   0
    
  4. # 4 楼答案

    更准确地说,API不一致性:

    虽然新的java.time-API总是使用TZDB的LMT信息,但我们还必须声明,旧的JDK类java.util.TimeZone在1900年进行了削减,其结果是1900年之前没有考虑LMT信息,但是1900年之后,是的,它仍然被考虑到了!只要在合适的区域做实验。。。(例如亚洲/堪察加半岛)

    我们不能说java.time-API的LMT策略或传统的1900策略是正确的。还要记住,有一个open JDK-issue来废除LMT战略。引文:

    The current TimeZone code does not use LMT. Joda-Time does, as does JSR-310. This is wrong.

    Recent discussion on the tzdb mailing list has indicated that the data is not properly maintained or reliably linked to the city of the zone ID. It is also relatively meaningless, being a notional value for a single city within a large region.

    Removing LMT is a good thing.

    甲骨文的沈学明在本期评论中说:

    The current j.u.TimeZone implementation DOES use LMT. If the LMT is defined/used cross the 1900.1.1 j.u.TimeZone cutoff date (by the tzdb data). For example the offset for Asia/Kamchatka from 1900.1.1 to the 1922.11.10 will be the LMT 10.34.36. Yes, if the LMT end date is before 1900.1.1, the LMT will not be used by the j.u.TZ.

    作为额外的历史注释,JDK问题最初是由java.time-API S.Colebourne的主要作者提出的,另请参见ancestoron Three Ten issue tracker