Почему вычитание меток времени эпохи-миллисекунд из 1927 дает неожиданный результат 353 вместо 1, когда времена отличаются всего на 1 секунду в Java? Это происходит при использовании SimpleDateFormat с часовым поясом Asia/Shanghai, и разница правильная при использовании дат из 1928.
Неожиданный результат в виде 353 вместо 1 при вычитании временных меток эпохи-миллисекунд из 1927 возникает из-за исторического разрыва в часовом поясе Asia/Shanghai. 31 декабря 1927 года в полночь часы были переведены назад на 5 минут 52 секунды (352 секунды), что создало временной разрыв, который SimpleDateFormat некорректно обрабатывает при расчете разницы эпох. Эта аномалия влияет только на даты вокруг этого конкретного исторического периода и не возникает для дат с 1928 года, поскольку изменение часового пояса уже было учтено.
Содержание
- Понимание изменения часового пояса Шанхая 1927 года
- Как SimpleDateFormat обрабатывает разрыв
- Технические детали проблемы расчета эпохи
- Решения и обходные пути
- Сравнение с другими часовыми поясами
Понимание изменения часового пояса Шанхая 1927 года
Основная причина этого неожиданного поведения коренится в значительном историческом изменении часового пояса в Шанхае. Согласно результатам исследований, в полночь в конце 1927 года часы в Шанхае были переведены назад на 5 минут 52 секунды. Это корректировка часового пояса была единовременным историческим событием, создавшим разрыв во временной шкале.
Это означает, что 31 декабря 1927 года на самом деле было два разных момента, когда местное время было “полночь”:
- Первая полночь (до корректировки)
- Вторая полночь (после перевода часов назад на 352 секунды)
Это создает ситуацию, в которой временной шкале не хватает 352 секунд, что SimpleDateFormat некорректно интерпретирует при расчете миллисекунд эпохи.
Ключевой факт: Корректировка в 5 минут 52 секунды в точности равна 352 секундам (5 × 60 + 52 = 352), что очень близко к наблюдаемой разнице в 353 в расчетах.
Как SimpleDateFormat обрабатывает разрыв
Когда SimpleDateFormat обрабатывает даты в часовом поясе Asia/Shanghai в декабре 1927 года, он сталкивается с этим историческим разрывом. Проблема проявляется несколькими способами:
Проблемы с расчетом миллисекунд эпохи
Метод getTime() в Java-классе Date возвращает миллисекунды с 1 января 1970 года (эпоха Unix). Когда SimpleDateFormat преобразует строку с датой в объект Date в этом проблемном часовом поясе, он вычисляет неверные значения эпохи из-за исторического скачка.
Пример проблемы:
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
sdf.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
Date date1 = sdf.parse("1927-12-31 23:59:59");
Date date2 = sdf.parse("1928-01-01 00:00:00");
// Ожидаемая разница: 1000 миллисекунд (1 секунда)
// Фактическая разница: 353000 миллисекунд (353 секунды)
long difference = date2.getTime() - date1.getTime(); // Возвращает 353000
Почему 353 вместо 1?
Разница в 353 вместо 1 возникает потому, что:
- SimpleDateFormat вычисляет значение эпохи для первой даты
- При обработке второй даты он учитывает исторический скачок часового пояса
- 352-секундный разрыв добавляется к ожидаемой разнице в 1 секунду
- В результате получается: 1 секунда (ожидаемо) + 352 секунды (скачок часового пояса) = 353 секунды всего
- Поскольку мы работаем с миллисекундами: 353 × 1000 = 353,000 миллисекунд
Технические детали проблемы расчета эпохи
Анализ отчета об ошибке Java
Согласно отчету об ошибке JDK-8035428, эта проблема была выявлена в Java 7u25, где расчет часового пояса Asia/Shanghai для 31 декабря 1927 года изменился. В отчете об ошибке указывается, что эта проблема была настолько значимой, что требовала исправления в среде выполнения Java.
Влияние базы данных часовых поясов
Проблема возникает из-за того, как Java реализует базу данных часовых поясов IANA. Запись часового пояса Asia/Shanghai содержит историческую информацию об этом изменении 1927 года. Когда SimpleDateFormat запрашивает эту базу данных для дат вокруг 31 декабря 1927 года, он получает информацию о разрыве.
Техническое объяснение:
- База данных часовых поясов содержит записи о том, когда происходили изменения часовых поясов
- Для Asia/Shanghai 1927-12-31 есть переход с UTC+08:00 на UTC+07:52:58
- Это создает разрыв в 352 секунд в местной временной шкале
- SimpleDateFormat применяет эту историческую информацию при расчете значений эпохи
- В результате даты, охватывающие этот переход, имеют неверные разницы значений эпохи
Затронутый диапазон дат
Эта проблема затрагивает даты, которые:
- Находятся в декабре 1927 года
- Охватывают переход полуночи 31 декабря 1927 года
- Обрабатываются в часовом поясе Asia/Shanghai
Даты с 1928 года и другие часовые поясы не затронуты, поскольку изменение часового пояса является историческим и не влияет на будущие расчеты.
Решения и обходные пути
1. Используйте API даты и времени Java 8+
Современный пакет Java 8 java.time корректнее обрабатывает исторические изменения часовых поясов:
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
.withZone(ZoneId.of("Asia/Shanghai"));
ZonedDateTime zdt1 = ZonedDateTime.parse("1927-12-31T23:59:59", formatter);
ZonedDateTime zdt2 = ZonedDateTime.parse("1928-01-01T00:00:00", formatter);
Duration duration = Duration.between(zdt1, zdt2);
System.out.println(duration.getSeconds()); // Должно быть 1
2. Избегайте проблемного периода часового пояса
Для приложений, которым нужно работать с датами из этого периода, рассмотрите:
- Использование другого часового пояса (например, UTC) для расчетов
- Избегание арифметики дат, которая пересекает переход 31 декабря 1927 года
- Использование часового пояса с фиксированным смещением вместо исторического Asia/Shanghai
3. Явная обработка разрыва
Если вы должны использовать SimpleDateFormat и часовой пояс Asia/Shanghai, вы можете обнаружить и обработать разрыв:
if (year == 1927 && month == Calendar.DECEMBER && day == 31) {
// Явная обработка специального случая
// Применение ручной корректировки для скачка часового пояса
}
4. Использование обновленных данных часовых поясов
Убедитесь, что вы используете последние данные часовых поясов. Java получает обновления своей базы данных часовых поясов через обновления установок JRE или JDK.
Сравнение с другими часовыми поясами
Часовой пояс Asia/Shanghai уникален в этом контексте из-за своего конкретного изменения в 1927 году. Большинство других часовых поясов не имеют таких драматичных разрывов в XX веке.
Ключевые различия:
- Гонконг: Похож на Шанхай, но без разрыва 1927 года
- Токио: Имеет исторические изменения, но другое время и масштаб
- UTC: Никогда не имеет разрывов, всегда последовательный
- Европа/Лондон: Имеет исторические изменения, но другие паттерны
Это объясняет, почему тот же код работает корректно с другими часовыми поясами, но не работает с Asia/Shanghai.
Практическое замечание: Разница в 353 вместо 1 часто является первым признаком, с которым сталкиваются разработчики при этой исторической проблеме часового пояса. Число 353 очень близко к 352 секундам фактического скачка часового пояса, что делает его четким индикатором проблемы.
Заключение
Неожиданный результат в виде 353 вместо 1 при вычитании временных меток эпохи-миллисекунд из 1927 года вызван историческим разрывом в часовом поясе Asia/Shanghai, где 31 декабря 1927 года часы были переведены назад на 5 минут 52 секунды. Это создает разрыв в 352 секунд во временной шкале, который SimpleDateFormat некорректно интерпретирует при расчете разницы эпохи.
Ключевые рекомендации:
- Обновитесь до Java 8+ и используйте пакет
java.timeдля лучшей обработки часовых поясов - Избегайте арифметики дат, которая пересекает 31 декабря 1927 года в часовом поясе Asia/Shanghai
- Используйте UTC или другие часовые пояса для расчетов, связанных с историческими датами
- Держите вашу установку Java обновленной для обеспечения последних данных часовых поясов
Понимание этой исторической аномалии важно для разработчиков, работающих с устаревшими системами или приложениями, которым нужно обрабатывать даты из этого конкретного периода. Эта проблема служит напоминанием о том, что обработка часовых поясов в программном обеспечении сложнее, чем кажется, и исторические изменения влияют на современные расчеты.
Источники
- Stack Overflow - Почему вычитание этих двух эпох-миллисекундных времен (в 1927 году) дает странный результат?
- База данных ошибок OpenJDK - JDK-8035428: Расчет часового пояса Asia/Shanghai для 31 декабря 1927 года изменился в 7u25
- Stack Overflow - Решение проблемы изменения времени в Шанхае 1927 года
- Stack Overflow - Как получить разницу без часовых поясов между двумя временами?
- Vlad Mihalcea - Руководство для начинающих по обработке часовых поясов в Java