1. Обзор
Летнее время , или DST, — это практика перевода часов в летние месяцы, чтобы использовать дополнительный час естественного света (экономия мощности отопления, мощности освещения, улучшение настроения и т. д.).
Он используется в нескольких странах , и его необходимо учитывать при работе с датами и временными метками.
В этом руководстве мы увидим, как правильно обрабатывать летнее время в Java в зависимости от разных мест.
2. Изменчивость JRE и DST
Во-первых, чрезвычайно важно понимать, что во всем мире зоны перехода на летнее время меняются очень часто , и нет никакого центрального органа, координирующего это.
Страна, а в некоторых случаях даже город, может решить, следует ли и как применять его или отозвать.
Каждый раз, когда это происходит, изменение записывается в базу данных часовых поясов IANA , и обновление будет развернуто в будущем выпуске JRE .
В случае, если ждать невозможно, мы можем принудительно перенести измененные данные часового пояса, содержащие новые настройки DST, в JRE с помощью официального инструмента Oracle под названием Java Time Zone Updater Tool , доступного на странице загрузки Java SE .
3. Неправильный путь: трехбуквенный идентификатор часового пояса
Еще во времена JDK 1.1 API разрешал трехбуквенные идентификаторы часовых поясов, но это приводило к нескольким проблемам.
Во-первых, это было связано с тем, что один и тот же трехбуквенный идентификатор мог относиться к нескольким часовым поясам. Например, CST
может быть «Центральным стандартным временем США», но также и «Стандартным временем Китая». Тогда платформа Java могла распознать только один из них.
Другая проблема заключалась в том, что стандартные часовые пояса никогда не учитывали переход на летнее время. В нескольких областях/регионах/городах местное летнее время может быть в пределах одного и того же стандартного часового пояса, поэтому стандартное время не соблюдает его.
Из-за обратной совместимости по-прежнему можно создать экземпляр java.util.Timezone
с трехбуквенным идентификатором. Однако этот метод устарел и больше не должен использоваться.
4. Правильный путь: идентификатор часового пояса TZDB
Правильный способ обработки летнего времени в Java — создать экземпляр часового
пояса с определенным идентификатором часового пояса TZDB, например. «Европа/Рим».
Затем мы будем использовать это в сочетании с классами, специфичными для времени, такими как java.util.Calendar
, чтобы получить правильную конфигурацию исходного смещения TimeZone
(по часовому поясу по Гринвичу) и автоматические корректировки перехода на летнее время.
Давайте посмотрим, как автоматически обрабатывается переход с GMT+1
на GMT+2
(который происходит в Италии 25 марта 2018 года в 02:00) при использовании правильного часового пояса:
TimeZone tz = TimeZone.getTimeZone("Europe/Rome");
TimeZone.setDefault(tz);
Calendar cal = Calendar.getInstance(tz, Locale.ITALIAN);
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.ITALIAN);
Date dateBeforeDST = df.parse("2018-03-25 01:55");
cal.setTime(dateBeforeDST);
assertThat(cal.get(Calendar.ZONE_OFFSET)).isEqualTo(3600000);
assertThat(cal.get(Calendar.DST_OFFSET)).isEqualTo(0);
Как мы видим, ZONE_OFFSET
составляет 60 минут (потому что в Италии GMT+1
), а DST_OFFSET
в это время равен 0.
Добавим в Календарь
десять минут :
cal.add(Calendar.MINUTE, 10);
Теперь DST_OFFSET
также стал 60-минутным, и страна перевела свое местное время с CET
(центральноевропейское время) на CEST
(центральноевропейское летнее время), то есть GMT+2
:
Date dateAfterDST = cal.getTime();
assertThat(cal.get(Calendar.DST_OFFSET))
.isEqualTo(3600000);
assertThat(dateAfterDST)
.isEqualTo(df.parse("2018-03-25 03:05"));
Если мы отобразим две даты в консоли, мы также увидим изменение часового пояса:
Before DST (00:55 UTC - 01:55 GMT+1) = Sun Mar 25 01:55:00 CET 2018
After DST (01:05 UTC - 03:05 GMT+2) = Sun Mar 25 03:05:00 CEST 2018
В качестве последнего теста мы можем измерить расстояние между двумя Date
s, 1:55 и 3:05:
Long deltaBetweenDatesInMillis = dateAfterDST.getTime() - dateBeforeDST.getTime();
Long tenMinutesInMillis = (1000L * 60 * 10);
assertThat(deltaBetweenDatesInMillis)
.isEqualTo(tenMinutesInMillis);
Как и следовало ожидать, дистанция составляет 10 минут вместо 70.
Мы увидели, как избежать распространенных ошибок, с которыми мы можем столкнуться при работе с Date
, благодаря правильному использованию TimeZone
и Locale
.
5. Лучший способ: Java 8 Date/Time API
Работа с этими небезопасными для потоков и не всегда удобными для пользователя классами java.util
всегда была сложной, особенно из-за проблем совместимости, которые мешали их надлежащему рефакторингу.
По этой причине в Java 8 появился совершенно новый пакет java.time
и совершенно новый набор API — Date/Time API. Это ISO-ориентированный, полностью потокобезопасный и вдохновленный знаменитой библиотекой Joda-Time.
Давайте подробнее рассмотрим эти новые классы, начиная с преемника java.util.Date
, java.time.LocalDateTime
:
LocalDateTime localDateTimeBeforeDST = LocalDateTime
.of(2018, 3, 25, 1, 55);
assertThat(localDateTimeBeforeDST.toString())
.isEqualTo("2018-03-25T01:55");
Мы можем наблюдать, как LocalDateTime
соответствует профилю ISO8601
, стандартному и широко распространенному обозначению даты и времени.
Однако он совершенно не знает Zones
и Offsets
, поэтому нам нужно преобразовать его в полностью поддерживающий DST java.time.ZonedDateTime
:
ZoneId italianZoneId = ZoneId.of("Europe/Rome");
ZonedDateTime zonedDateTimeBeforeDST = localDateTimeBeforeDST
.atZone(italianZoneId);
assertThat(zonedDateTimeBeforeDST.toString())
.isEqualTo("2018-03-25T01:55+01:00[Europe/Rome]");
Как мы видим, теперь дата включает в себя две фундаментальные завершающие части информации: +01:00
— это ZoneOffset
, а [Europe/Rome]
— это ZoneId
.
Как и в предыдущем примере, давайте активируем летнее время, добавив десять минут:
ZonedDateTime zonedDateTimeAfterDST = zonedDateTimeBeforeDST
.plus(10, ChronoUnit.MINUTES);
assertThat(zonedDateTimeAfterDST.toString())
.isEqualTo("2018-03-25T03:05+02:00[Europe/Rome]");
Опять же, мы видим, как и время, и смещение зоны смещаются вперед, сохраняя при этом то же расстояние:
Long deltaBetweenDatesInMinutes = ChronoUnit.MINUTES
.between(zonedDateTimeBeforeDST,zonedDateTimeAfterDST);
assertThat(deltaBetweenDatesInMinutes)
.isEqualTo(10);
6. Заключение
Мы увидели, что такое переход на летнее время и как с ним обращаться, на некоторых практических примерах в различных версиях основного API Java.
При работе с Java 8 и более поздних версий использование нового пакета java.time
рекомендуется благодаря простоте использования и его стандартному поточно-ориентированному характеру.
Как всегда, полный исходный код доступен на Github .