Avatar ·

Почему вычитание миллисекунд эпохи (1927 год) даёт странный результат?

Всем привет! Я новичок в Java и столкнулся с очень странной проблемой при работе с датами. Пытаюсь вычислить разницу в миллисекундах между двумя моментами времени в 1927 году, но результат получается не таким, как я ожидаю. Вот мой код: ```java import java.text.SimpleDateFormat; import java.util.Date; import java.util.TimeZone; public class StrangeDate { public static void main(String[] args) throws Exception { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); sdf.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai")); // Дата 1: 1927-12-31 23:54:07 Date date1 = sdf.parse("1927-12-31 23:54:07"); // Дата 2: 1927-12-31 23:54:08 Date date2 = sdf.parse("1927-12-31 23:54:08"); long diff = date2.getTime() - date1.getTime(); System.out.println("Разница в миллисекундах: " + diff); // Ожидаю: 1000 (1 секунда) // Получаю: 353 (около 0.35 секунды!) } } ``` Я ожидал, что разница будет ровно 1000 миллисекунд (1 секунда), но получаю 353. При этом если я беру даты из 2024 года, всё работает нормально. В чём может быть дело? Это баг Java или я что-то делаю не так? Использую Java 8. <<>>

Проблема: исторические изменения часовых поясов

Вы столкнулись с классическим эффектом, связанным с историческими корректировками часовых поясов. Дело не в баге Java, а в реальном историческом событии: в 1927 году в Шанхае (и вообще в Китае) произошёл переход на другое время.

Что произошло в 1927 году?

До 1 января 1928 года в Шанхае использовалось местное среднее солнечное время (LMT). 31 декабря 1927 года в 23:54:08 по местному времени был произведён перевод часов на 5 минут 52 секунды назад, чтобы синхронизировать время с поясом UTC+8. Это означает, что между 23:54:07 и 23:54:08 фактически прошло не 1 секунда, а 353 миллисекунды (потому что часы "откатились" на 5 минут 52 секунды, и момент 23:54:08 наступил почти сразу после 23:54:07).

Вот как это выглядит на временной шкале (UTC):

23:54:07 LMT (UTC+8:05:52) → 15:48:15 UTC
23:54:08 CST (UTC+8:00)   → 15:54:08 UTC
Разница: 15:54:08 - 15:48:15 = 5 минут 53 секунды = 353 секунды? Нет!

На самом деле разница в UTC составляет 5 минут 53 секунды, но в местном времени между этими двумя моментами прошло всего 1 секунда. Java правильно вычисляет разницу в миллисекундах эпохи (UTC), поэтому date2.getTime() - date1.getTime() даёт реальную разницу по UTC — 353 000 миллисекунд (353 секунды).

Почему вы ожидали 1000 мс?

Вы ожидали, что разница в местном времени (1 секунда) будет равна разнице в UTC (1000 мс). Но из-за сдвига часового пояса это не так. Ваш код преобразует строки в Date, используя часовой пояс Asia/Shanghai, который учитывает исторические изменения.

Как исправить?

Если вам нужна разница именно в местном времени (без учёта исторических сдвигов), используйте java.time пакет из Java 8:

import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;

public class FixedDate {
    public static void main(String[] args) {
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        
        LocalDateTime ldt1 = LocalDateTime.parse("1927-12-31 23:54:07", dtf);
        LocalDateTime ldt2 = LocalDateTime.parse("1927-12-31 23:54:08", dtf);
        
        long diff = ldt2.toEpochSecond(ZoneOffset.UTC) - ldt1.toEpochSecond(ZoneOffset.UTC);
        System.out.println("Разница (в UTC): " + diff + " секунд");
        // Теперь будет 1 секунда, но это уже не реальное историческое время
    }
}

Но помните: исторические данные важны. Если вы работаете с реальными историческими событиями, то правильный ответ — 353 секунды. Если вам нужна "наивная" разница — используйте LocalDateTime без часового пояса.

Вывод

  • Не все дни длятся 24 часа — из-за перевода часов некоторые дни короче или длиннее.
  • Java правильно обрабатывает исторические данные — база данных часовых поясов (IANA TZDB) содержит все известные изменения.
  • Используйте java.time — старые классы Date и SimpleDateFormat устарели и могут давать неожиданные результаты.
<<>>

Альтернативный подход: использование Instant и Duration

Если вам нужно получить именно разницу в миллисекундах между двумя моментами времени с учётом часового пояса, но без сюрпризов от исторических данных, можно использовать java.time.Instant и java.time.Duration:

import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;

public class Alternative {
    public static void main(String[] args) {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        ZoneId shanghai = ZoneId.of("Asia/Shanghai");
        
        ZonedDateTime zdt1 = ZonedDateTime.of(
            LocalDateTime.parse("1927-12-31 23:54:07", formatter), shanghai);
        ZonedDateTime zdt2 = ZonedDateTime.of(
            LocalDateTime.parse("1927-12-31 23:54:08", formatter), shanghai);
        
        Instant instant1 = zdt1.toInstant();
        Instant instant2 = zdt2.toInstant();
        
        long diffMillis = Duration.between(instant1, instant2).toMillis();
        System.out.println("Реальная разница: " + diffMillis + " мс");
        // Выведет 353000 (из-за исторического сдвига)
    }
}

Этот подход явно показывает, что разница во времени между двумя "соседними" секундами в 1927 году в Шанхае составляет 353 секунды (5 минут 53 секунды) по UTC. Если вы хотите игнорировать исторические данные, просто используйте LocalDateTime с фиксированным смещением, как показано в первом ответе.