Avatar ·

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

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

Краткое объяснение проблемы

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

Почему разница отрицательная?

Давайте разберёмся по шагам:

  • Исторический контекст: До 1928 года в Шанхае использовалось местное солнечное время (LMT — Local Mean Time). В полночь 1 января 1928 года Китай перешёл на единый часовой пояс UTC+8. Однако в базе данных часовых поясов (IANA Time Zone Database) это событие зафиксировано как произошедшее 31 декабря 1927 года в 23:54:08 по местному времени.
  • Что произошло: В 23:54:08 по шанхайскому времени часы были переведены назад на 5 минут и 52 секунды, чтобы синхронизироваться с UTC+8. То есть после 23:54:07 наступило 23:54:08, но затем время "откатилось" до 23:54:00 (по новому времени).
  • Как это отражается в коде: Когда вы парсите строку "1927-12-31 23:54:08" с часовым поясом "Asia/Shanghai", Java интерпретирует её как момент до перевода стрелок (по старому времени). А "1927-12-31 23:54:07" — это момент после перевода (по новому времени). Из-за этого date2 (23:54:08) оказывается раньше date1 (23:54:07) в абсолютном времени (UTC), что даёт отрицательную разницу.

Как правильно считать разницу?

Если вам нужна чистая разница в миллисекундах между двумя моментами, используйте UTC (или GMT) для парсинга. Это исключит влияние часовых поясов:

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;

public class CorrectDifference {
    public static void main(String[] args) throws Exception {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        sdf.setTimeZone(TimeZone.getTimeZone("UTC")); // Используем UTC

        Date date1 = sdf.parse("1927-12-31 23:54:07");
        Date date2 = sdf.parse("1927-12-31 23:54:08");

        long diffInMillis = date2.getTime() - date1.getTime();
        System.out.println("Разница в миллисекундах (UTC): " + diffInMillis); // Выведет 1000
    }
}

Современный подход (Java 8+)

Старые классы Date и SimpleDateFormat считаются устаревшими. Используйте новый API java.time:

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

public class ModernSolution {
    public static void main(String[] args) {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        
        // Парсим с учётом часового пояса
        ZonedDateTime zdt1 = ZonedDateTime.of(
            java.time.LocalDateTime.parse("1927-12-31 23:54:07", formatter),
            ZoneId.of("Asia/Shanghai")
        );
        ZonedDateTime zdt2 = ZonedDateTime.of(
            java.time.LocalDateTime.parse("1927-12-31 23:54:08", formatter),
            ZoneId.of("Asia/Shanghai")
        );

        // Переводим в Instant (UTC) и считаем разницу
        long diffMillis = zdt2.toInstant().toEpochMilli() - zdt1.toInstant().toEpochMilli();
        System.out.println("Разница с учётом истории: " + diffMillis); // -353
        
        // Если нужно игнорировать историю — используем LocalDateTime
        long diffLocal = java.time.Duration.between(
            java.time.LocalDateTime.parse("1927-12-31 23:54:07", formatter),
            java.time.LocalDateTime.parse("1927-12-31 23:54:08", formatter)
        ).toMillis();
        System.out.println("Разница без учёта истории: " + diffLocal); // 1000
    }
}

Выводы

  • Всегда помните, что часовые пояса — это не просто смещение от UTC. Они содержат историю изменений (переходы на летнее время, смены поясов).
  • Для точных расчётов разницы между двумя моментами всегда используйте UTC или класс Instant.
  • Если вам не важна историческая точность, работайте с LocalDateTime (без часового пояса).

Ваш код не сломан — он просто показывает реальный исторический факт! Это отличный пример того, как важно понимать работу с датами и временем.

<<>>

Альтернативный подход: используйте библиотеку Joda-Time

Если вы не можете перейти на Java 8, но хотите избежать проблем со старыми классами, используйте Joda-Time — это де-факто стандарт для работы с датами до Java 8. Вот как решить вашу задачу:

import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.DateTimeFormat;

public class JodaTimeExample {
    public static void main(String[] args) {
        DateTimeFormatter fmt = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss");
        
        DateTime dt1 = fmt.withZone(DateTimeZone.forID("Asia/Shanghai"))
                          .parseDateTime("1927-12-31 23:54:07");
        DateTime dt2 = fmt.withZone(DateTimeZone.forID("Asia/Shanghai"))
                          .parseDateTime("1927-12-31 23:54:08");

        long diff = dt2.getMillis() - dt1.getMillis();
        System.out.println("Разница (Joda-Time): " + diff); // -353

        // Если нужно игнорировать историю — используйте LocalDateTime
        long diffLocal = org.joda.time.Duration
            .fieldDifference(
                fmt.parseLocalDateTime("1927-12-31 23:54:07"),
                fmt.parseLocalDateTime("1927-12-31 23:54:08")
            )
            .getMillis();
        System.out.println("Разница без истории: " + diffLocal); // 1000
    }
}

Joda-Time автоматически учитывает все исторические изменения часовых поясов, как и java.time. Но она работает на Java 6 и 7. Рекомендую использовать её, если вы застряли на старых версиях Java.

Важно: Не пытайтесь "исправить" результат, добавляя или вычитая константы. Это приведёт к ошибкам для других дат. Всегда используйте правильные инструменты для работы с часовыми поясами.