Перейти к основному содержимому

Обработка перехода на летнее время в Java

· 5 мин. чтения

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 .