Почему вычитание двух миллисекундных меток времени (1927 год) даёт странный результат?
Краткое объяснение проблемы
Вы столкнулись с историческим артефактом часовых поясов. В 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.
Важно: Не пытайтесь "исправить" результат, добавляя или вычитая константы. Это приведёт к ошибкам для других дат. Всегда используйте правильные инструменты для работы с часовыми поясами.