Почему разница дат в 1927 году ломает код? Топ ошибок Java

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

1927 год: Тот самый момент, когда время пошло вспять

Представьте: вы пишете на Java простой скрипт для расчёта разницы между двумя датами в миллисекундах. Берёте 1927 год, вычитаете одно значение из другого — и получаете не 3600 секунд (один час), а странное число. Вы перепроверяете код, гуглите, находите легендарный вопрос на StackOverflow с тысячами просмотров. И понимаете: вы не одиноки.

Этот баг — настоящий артефакт истории, который до сих пор пугает новичков. В этой статье мы разберём почему вычитание epoch-milli times в 1927 году даёт странный результат, покажем на реальных примерах кода Java и, главное, дадим рабочие способы избежать этой ловушки. Если вы работаете с историческими данными или просто хотите писать надёжный код — эта информация для вас.

Суть проблемы: Магия 31 декабря 1927 года

Давайте сразу к коду. Вот классический пример, который ломает мозг:

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

public class StrangeDateDifference {
    public static void main(String[] args) throws Exception {
        TimeZone tz = TimeZone.getTimeZone("Asia/Shanghai");
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        sdf.setTimeZone(tz);

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

        long diff = date2.getTime() - date1.getTime();
        System.out.println("Разница в миллисекундах: " + diff);
        // Ожидаем 1000, но получаем...
    }
}

Запускаете — и вместо 1000 миллисекунд (1 секунда) видите 353? Или даже отрицательное число? Поздравляю, вы столкнулись с ошибкой вычитания дат 1927 года.

Причина не в баге Java (хотя и в нём тоже, но об этом позже). Причина — в историческом изменении часового пояса. 31 декабря 1927 года в Шанхае (Asia/Shanghai) произошло странное: в 23:54:08 местное время перевели на 5 минут 52 секунды назад. Фактически, секунда длилась не 1000 мс, а всего 353 мс. Java честно отрабатывает этот исторический факт, но для разработчика это выглядит как магия.

Топ ошибок, которые совершают разработчики

1. Слепое доверие старым API (java.util.Date и SimpleDateFormat)

Первая и самая популярная ошибка — использование устаревших классов java.util.Date и SimpleDateFormat. Они не только неудобны, но и содержат скрытые сюрпризы, особенно при работе с историческими датами.

Как избежать: Используйте java.time (Java 8+). Например:

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

public class SafeDateDifference {
    public static void main(String[] args) {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        ZoneId zone = ZoneId.of("Asia/Shanghai");

        LocalDateTime ldt1 = LocalDateTime.parse("1927-12-31 23:54:07", formatter);
        LocalDateTime ldt2 = LocalDateTime.parse("1927-12-31 23:54:08", formatter);

        ZonedDateTime zdt1 = ldt1.atZone(zone);
        ZonedDateTime zdt2 = ldt2.atZone(zone);

        long diff = zdt2.toInstant().toEpochMilli() - zdt1.toInstant().toEpochMilli();
        System.out.println("Разница в миллисекундах: " + diff);
        // Всё равно получите странное число, но хотя бы будете знать почему!
    }
}

2. Игнорирование исторических изменений часовых поясов

Многие думают, что часовые пояса — это просто фиксированный сдвиг относительно UTC. Но на самом деле они менялись десятки раз за последние 100 лет. В 1927 году Шанхай перешёл на «постоянное летнее время», а потом обратно — отсюда и скачок.

Как избежать: Всегда проверяйте историю таймзоны через ZoneRules:

ZoneId zone = ZoneId.of("Asia/Shanghai");
ZoneRules rules = zone.getRules();
System.out.println(rules.getTransitionRules()); // Покажет все переходы

3. Предположение, что 1 секунда = 1000 мс всегда

Это кажется очевидным, но из-за високосных секунд и исторических переходов это не так. В Java java.time корректно обрабатывает такие случаи, но если вы работаете с «сырыми» миллисекундами, будьте готовы к сюрпризам.

Как избежать: Используйте Duration для измерения времени:

import java.time.Duration;
import java.time.ZonedDateTime;

Duration duration = Duration.between(zdt1, zdt2);
System.out.println("Длительность: " + duration.getSeconds() + " секунд");
// Корректно покажет 0 секунд (если переход был в эту секунду)

Почему это важно для современного Java-разработчика?

Казалось бы, 1927 год — далёкое прошлое. Но подобные баги всплывают постоянно:

  • Финансовые системы — расчёт процентов за исторические периоды может дать неверную сумму.
  • Логи и аудит — неправильная разница во времени может исказить хронологию событий.
  • Научные расчёты — астрономия, геология, история требуют точных временных интервалов.

Особенно опасна ошибка вычитания дат 1927 года в тестах. Вы пишете юнит-тест, ожидаете 1000 мс, получаете 353 — и тест падает. Хорошо, если вы знаете причину. Плохо — если начинаете «подгонять» код под ожидания.

Как правильно работать с датами в Java (рекомендации)

Для новых проектов

Используйте только java.time (Java 8+). Забудьте про Date и Calendar. Вот безопасный шаблон:

import java.time.*;
import java.time.format.*;

public class ModernDateExample {
    public static void main(String[] args) {
        // 1. Парсим строку
        DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        LocalDateTime ldt = LocalDateTime.parse("1927-12-31 23:54:07", fmt);
        
        // 2. Привязываем к зоне
        ZoneId zone = ZoneId.of("Asia/Shanghai");
        ZonedDateTime zdt = ldt.atZone(zone);
        
        // 3. Работаем с Instant (epoch milli)
        long epochMilli = zdt.toInstant().toEpochMilli();
        System.out.println("Epoch milli: " + epochMilli);
        
        // 4                

Блоги

Рекомендаций по книгам