某些日期无法在 Java 中正确转换为特定时区午夜的纪元时间戳

Some dates cannot be converted correctly in Java to an epoch timestamps at the midnight of a specific timezone

这个 Java 代码,给定一个字符串日期,应该在 CET 区域的午夜打印同一日期的纪元时间戳(假设我不在同一个区域)。

public static void main(String[] args) throws ParseException {

    String dateStr = "1995-06-06";

    SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
    formatter.setTimeZone(TimeZone.getTimeZone("CET"));
    Date date = formatter.parse(dateStr);
    Calendar c = new GregorianCalendar();
    c.setTimeZone(TimeZone.getTimeZone("CET"));
    c.setTime(date);

    c.set(Calendar.HOUR_OF_DAY, 0);
    c.set(Calendar.MINUTE, 0);
    c.set(Calendar.SECOND, 0);
    c.set(Calendar.MILLISECOND, 0);

    System.out.println("Epoch timestamp = " + c.getTime().getTime());
}

如果我运行上面的程序我应该得到打印:

 Epoch timestamp = 802389600000

我可以在这里验证它是否正确:

https://www.epochconverter.com/timezones?q=802389600&tz=Europe%2FMalta

现在,这适用于大多数日期。然而,有一些奇怪的日期,如“1975-09-19”,它不起作用。事实上,它生成 180313200000 作为时间戳,它给出的是凌晨 1 点而不是午夜:

https://www.epochconverter.com/timezones?q=180313200&tz=Europe%2FMalta

你能解释一下为什么吗?我错过了什么?

您的日期示例之间的夏令时似乎有所不同。
如果我使用 java.time(自 Java 8 起应始终使用),我会得到具有不同偏移量的结果:

  • "+02:00" 对于 "1995-06-06"
  • "+01:00" 对于 "1975-09-19"

我是这样得到结果的:

public static void main(String[] args) {
    // provide two sample dates
    String workingDateStr = "1995-06-06";
    String failingDateStr = "1975-09-19";
    // and a formatter that parses the format
    DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    // then parse them to date objects that don't know about time or zone
    LocalDate workingDate = LocalDate.parse(workingDateStr, dtf);
    LocalDate failingDate = LocalDate.parse(failingDateStr, dtf);
    /*
     *  then create an objects that are aware of time and zone
     *  by using the parsed dates, adding a time of 00:00:00 and a zone
     */
    ZonedDateTime workingZdt = ZonedDateTime.of(workingDate, LocalTime.MIN, ZoneId.of("CET"));
    ZonedDateTime failingZdt = ZonedDateTime.of(failingDate, LocalTime.MIN, ZoneId.of("CET"));

    // finally, print different representations of the results
    System.out.println(workingZdt + " ——> " + workingZdt.toInstant().toEpochMilli());
    System.out.println(failingZdt + " ——> " + failingZdt.toInstant().toEpochMilli());
}

输出:

1995-06-06T00:00+02:00[CET] ——> 802389600000
1975-09-19T00:00+01:00[CET] ——> 180313200000

这意味着您最好使用特定的偏移量而不是区域。

这个问题可能是由于马耳他引入夏令时的时间问题,请查看以下代码及其输出:

public static void main(String[] args) {
    // provide two sample dates
    String failingDateStr = "1975-09-19";
    // and a formatter that parses the format
    DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    // then parse them to date objects that don't know about time or zone
    LocalDate failingDate = LocalDate.parse(failingDateStr, dtf);
    /*
     *  then create an objects that are aware of time and zone
     *  by using the parsed dates, adding a time of 00:00:00 and a zone
     */
    ZonedDateTime failingZdt = ZonedDateTime.of(failingDate, LocalTime.MIN, ZoneId.of("CET"));

    // add some years to 1975 and...
    for (int year = 0; year < 4; year++) {
        // ... print the different representations of the result
        System.out.println(failingZdt.plusYears(year) + " ——> " 
                + failingZdt.plusYears(year).toInstant().toEpochMilli());
    }
}

输出:

1975-09-19T00:00+01:00[CET] ——> 180313200000
1976-09-19T00:00+01:00[CET] ——> 211935600000
1977-09-19T00:00+02:00[CET] ——> 243468000000
1978-09-19T00:00+02:00[CET] ——> 275004000000

此输出表示 1977 年的介绍...对吗?

时区差异

您的 Java 代码使用 CET,这不是真正的时区(例如,因为大多数使用它的地区在一年中的大部分时间都使用 CEST)。 Java 将 CET 转换为 Europe/Paris。法国和巴黎在 1975 年不使用夏令时 (DST)。1976 年 3 月重新引入。

您的 link 到纪元转换器指定了马耳他时区 (Europe/Malta)。马耳他 确实 在 1975 年使用夏令时:当年 4 月 20 日至 9 月 21 日是 CEST。

这解释了结果的差异。

在Java代码中

如果你想要马耳他时间:

    String dateStr = "1975-09-19";

    long epochTimestamp = 
            LocalDate 
            .parse(dateStr)                            
            .atStartOfDay(ZoneId.of("Europe/Malta"))
            .toInstant()
            .toEpochMilli();

    System.out.println("Epoch timestamp = " + epochTimestamp);

这会打印:

Epoch timestamp = 180309600000

并且您 link 访问的纪元转换器很乐意同意:

Conversion results (180309600)

180309600 converts to Friday September 19, 1975 00:00:00 (am) in time zone Europe/Malta (CEST) The offset (difference to Greenwich Time/GMT) is +02:00 or in seconds 7200. This date is in daylight saving time.

在 Java 中,请使用现代 Java 日期和时间 API java.time 作为您的日期和时间工作。与 SimpleDateFormatTimeZoneDateCalendar 等旧日期和时间 类 相比,使用它要好得多。将小时等设置为 0 也不是获取当天第一时刻的正确方法。有些情况下夏令时从一天的开始开始,所以一天的第一时刻是 01:00:00。 Java 知道这一点,因此 atStartOfDay 方法将为您提供当天的正确时刻。

并且无论使用过时的还是现代的 类 始终以 region/city 格式指定时区,例如 Europe/Paris 或 Europe/Malta。三、四和五个字母的时区缩写通常含糊不清,通常不是真正的时区,因此不可依赖。

链接