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