Почему вычитание этих двух временных меток (эпохи в миллисекундах) из 1927 года даёт странный результат?
Описание проблемы:
Я столкнулся с неожиданным поведением в Java, когда пытаюсь сравнить две строки дат, которые отличаются друг от друга на одну секунду. В следующем коде я парслю две строки, представляющие время, разница между которыми составляет 1 секунду, и сравниваю их:
public static void main(String[] args) throws ParseException {
SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String str3 = "1927-12-31 23:54:07";
String str4 = "1927-12-31 23:54:08";
Date sDt3 = sf.parse(str3);
Date sDt4 = sf.parse(str4);
long ld3 = sDt3.getTime() / 1000;
long ld4 = sDt4.getTime() / 1000;
System.out.println(ld4 - ld3);
}
Ожидаемый вывод:
При выводе значения ld4 - ld3
, я ожидаю получить 1
, поскольку строки дат отличаются на одну секунду.
Фактический вывод:
Однако, вместо ожидаемого результата, выводится 353
.
Я также заметил, что если я изменю строки дат, чтобы они представляли время, которое отличается на одну секунду, то:
String str3 = "1927-12-31 23:54:08";
String str4 = "1927-12-31 23:54:09";
В этом случае вывод действительно будет 1
.
Дополнительная информация:
- Версия Java:
java version "1.6.0_22"
Java(TM) SE Runtime Environment (build 1.6.0_22-b04)
Dynamic Code Evolution Client VM (build 0.2-b02-internal, 19.0-b04-internal, mixed mode)
- Часовой пояс (
TimeZone.getDefault()
):
sun.util.calendar.ZoneInfo[id="Asia/Shanghai", offset=28800000, dstSavings=0, useDaylight=false, transitions=19, lastRule=null]
- Локаль (
Locale.getDefault()
):
zh_CN
Буду признателен за любую помощь в понимании, почему ld4 - ld3
не дает ожидаемого результата.
4 ответ(ов)
Мораль этой странности такова:
- Используйте даты и времена в формате UTC, когда это возможно.
- Если вы не можете отображать дату или время в UTC, обязательно указывайте часовой пояс.
- Если вы не можете требовать ввод даты/времени в UTC, требуйте явно указанный часовой пояс.
Вместо того, чтобы конвертировать каждую дату, вы можете использовать следующий код:
long difference = (sDt4.getTime() - sDt3.getTime()) / 1000;
System.out.println(difference);
Таким образом, вы получите разницу между датами в секундах. Например, в вашем случае результат будет:
1
Это означает, что между sDt3
и sDt4
разница составляет 1 секунду.
Как объяснили другие участники, здесь есть временная дисконнектность. Для времени 1927-12-31 23:54:08
в часовом поясе Asia/Shanghai
существует два возможных смещения по времени, тогда как для 1927-12-31 23:54:07
- только одно. Таким образом, в зависимости от того, какое смещение используется, разница будет составлять либо одну секунду, либо пять минут и 53 секунды.
Этот небольшой сдвиг смещений, а не привычное нам одночасовое летнее время, немного затемняет проблему.
Обратите внимание, что обновление базы данных часовых поясов 2013a переместило это разрывное состояние на несколько секунд раньше, но эффект все равно будет наблюдаем.
Новый пакет java.time
в Java 8 позволяет нам видеть эту проблему более отчетливо и предоставляет инструменты для ее решения. Предположим, у нас есть код:
DateTimeFormatterBuilder dtfb = new DateTimeFormatterBuilder();
dtfb.append(DateTimeFormatter.ISO_LOCAL_DATE);
dtfb.appendLiteral(' ');
dtfb.append(DateTimeFormatter.ISO_LOCAL_TIME);
DateTimeFormatter dtf = dtfb.toFormatter();
ZoneId shanghai = ZoneId.of("Asia/Shanghai");
String str3 = "1927-12-31 23:54:07";
String str4 = "1927-12-31 23:54:08";
ZonedDateTime zdt3 = LocalDateTime.parse(str3, dtf).atZone(shanghai);
ZonedDateTime zdt4 = LocalDateTime.parse(str4, dtf).atZone(shanghai);
Duration durationAtEarlierOffset = Duration.between(zdt3.withEarlierOffsetAtOverlap(), zdt4.withEarlierOffsetAtOverlap());
Duration durationAtLaterOffset = Duration.between(zdt3.withLaterOffsetAtOverlap(), zdt4.withLaterOffsetAtOverlap());
В этом случае durationAtEarlierOffset
будет равна одной секунде, в то время как durationAtLaterOffset
составит пять минут и 53 секунды.
Также эти два смещения одинаковы:
// Оба имеют смещение +08:05:52
ZoneOffset zo3Earlier = zdt3.withEarlierOffsetAtOverlap().getOffset();
ZoneOffset zo3Later = zdt3.withLaterOffsetAtOverlap().getOffset();
Но эти два смещения разные:
// +08:05:52
ZoneOffset zo4Earlier = zdt4.withEarlierOffsetAtOverlap().getOffset();
// +08:00
ZoneOffset zo4Later = zdt4.withLaterOffsetAtOverlap().getOffset();
Вы можете заметить ту же проблему, сравнивая 1927-12-31 23:59:59
с 1928-01-01 00:00:00
, хотя в этом случае именно более раннее смещение дает более длительное расхождение, и именно более ранняя дата имеет два возможных смещения.
Другим способом подойти к этой проблеме является проверка наличия перехода. Мы можем сделать это следующим образом:
// Null
ZoneOffsetTransition zot3 = shanghai.getRules().getTransition(ld3.toLocalDateTime);
// Переход с наложением
ZoneOffsetTransition zot4 = shanghai.getRules().getTransition(ld4.toLocalDateTime);
Вы можете проверить, является ли переход наложением (где есть более одного действительного смещения для данной даты/времени) или разрывом (где эта дата/время недействительна для данного идентификатора часового пояса) с помощью методов isOverlap()
и isGap()
для zot4
.
Надеюсь, это поможет решить подобные вопросы, когда Java 8 станет широко доступна, или тем, кто использует Java 7 и применяет обратную совместимость JSR 310.
Как упоминали другие, речь идет о изменении времени в 1927 году в Шанхае.
В Шанхае в местном стандартном времени было 23:54:07
, но через 5 минут 52 секунды наступил следующий день, и время изменилось на 00:00:00
. Затем местное стандартное время изменилось на 23:54:08
. Поэтому разница между двумя временами составляет 343 секунды, а не 1 секунду, как можно было бы ожидать.
Временные изменения могут происходить и в других местах, например, в США. В США существует переход на летнее время. Когда начинается переход на летнее время, часы сдвигаются вперед на 1 час. Но через некоторое время переход заканчивается, и часы откатываются назад на 1 час к стандартному времени. Поэтому иногда, сравнивая время в США, разница может составлять около 3600
секунд, а не 1 секунду.
Однако существует разница между этими двумя изменениями времени. Второе изменение происходит постоянно, в то время как первое было одноразовым. Оно не изменялось назад или не корректировалось снова на такую же величину.
Лучше использовать UTC, если не требуется использовать не-UTC время, например, для отображения.
Сортировка ArrayList пользовательских объектов по свойству
Преобразование строки в дату в Java
Что значит "Не удалось найти или загрузить основной класс"?
Почему в RecyclerView отсутствует onItemClickListener()?
Что значит 'synchronized'?