Рубрики
Без рубрики

Руководство для начинающих по обработке часовых поясов Java

Узнайте, как Java обрабатывает информацию о часовом поясе для даты, метки времени, календаря или даты и времени Joda при анализе меток времени из строки.

Автор оригинала: Vlad Mihalcea.

Основные понятия о времени

Большинство веб-приложений должны поддерживать разные часовые пояса, и правильно управлять часовыми поясами непросто. Что еще хуже, вы должны убедиться, что временные метки согласованы на различных языках программирования (например, JavaScript на интерфейсе, Java на среднем уровне и MongoDB в качестве хранилища данных). Цель этого поста-объяснить основные понятия абсолютного и относительного времени.

Эпоха

/| Эпоха – это абсолютная временная привязка. Большинство языков программирования (например, Java, JavaScript, Python) используют эпоху Unix (полночь 1 января 1970 года) при выражении заданной метки времени в виде количества миллисекунд, прошедших с момента привязки к фиксированному моменту времени.

Относительная числовая метка времени

Относительная числовая метка времени выражается как количество миллисекунд, прошедших с начала эпохи.

Часовой пояс

Всемирное координированное время (UTC) является наиболее распространенным стандартом времени. Часовой пояс UTC (эквивалентный GMT ) представляет собой временную привязку, к которой относятся все остальные часовые пояса (через положительное/отрицательное смещение).

Часовой пояс UTC обычно называют временем Зулу (Z) или UTC+0. Часовой пояс Японии-UTC+9, а часовой пояс Гонолулу-UTC-10. Во времена эпохи Unix (1 января 1970 года 00:00 по часовому поясу UTC) это было 1 января 1970 года 09:00 в Токио и 31 декабря 1969 года 14:00 в Гонолулу.

ISO 8601

ISO 8601 является наиболее распространенным стандартом представления даты/времени и использует следующие форматы даты/времени:

1970-01-01T00:00:00.000+00:00 UTC
1970-01-01T00:00:00.000 Z UTC зулусское время
1970-01-01T09:00:00.000+09:00 Токио
1969-12-31T14:00:00.000-10:00 Гонолулу

Основы Java time

Основы Java time

java.util.Дата определенно является наиболее распространенным классом, связанным со временем. Он представляет собой фиксированный момент времени, выраженный как относительное количество миллисекунд, прошедших с эпохи. java.util.Дата не зависит от часового пояса , за исключением метода toString , который использует локальный часовой пояс для создания строкового представления.

java.util.Дата || определенно является наиболее распространенным классом, связанным со временем. Он представляет собой фиксированный момент времени, выраженный как относительное количество миллисекунд, прошедших с эпохи. java.util.Дата || не зависит от часового пояса || , за исключением метода || toString||, который использует || локальный часовой пояс || для создания строкового представления.

Файл java.util.Календарь является одновременно фабрикой даты/времени, а также экземпляром времени с учетом часового пояса. Это один из наименее удобных для работы классов Java API, и мы можем продемонстрировать это в следующем примере:

@Test
public void testTimeZonesWithCalendar() throws ParseException {
    assertEquals(
        0L, 
        newCalendarInstanceMillis("GMT").getTimeInMillis()
    );
    
    assertEquals(
        TimeUnit.HOURS.toMillis(-9), 
        newCalendarInstanceMillis("Japan").getTimeInMillis()
    );
    
    assertEquals(
        TimeUnit.HOURS.toMillis(10), 
        newCalendarInstanceMillis("Pacific/Honolulu").getTimeInMillis()
    );
    
    Calendar epoch = newCalendarInstanceMillis("GMT");
    epoch.setTimeZone(TimeZone.getTimeZone("Japan"));
    
    assertEquals(
        TimeUnit.HOURS.toMillis(-9), 
        epoch.getTimeInMillis()
    );
}

private Calendar newCalendarInstanceMillis(
        String timeZoneId) {
    Calendar calendar = new GregorianCalendar();
    calendar.set(Calendar.YEAR, 1970);
    calendar.set(Calendar.MONTH, 0);
    calendar.set(Calendar.DAY_OF_MONTH, 1);
    calendar.set(Calendar.HOUR_OF_DAY, 0);
    calendar.set(Calendar.MINUTE, 0);
    calendar.set(Calendar.SECOND, 0);
    calendar.set(Calendar.MILLISECOND, 0);
    calendar.setTimeZone(TimeZone.getTimeZone(timeZoneId));
    return calendar;
}

Во время эпохи Unix (часовой пояс UTC) токийское время опережало на девять часов, в то время как Гонолулу отставал на десять часов.

Изменение часового пояса календаря сохраняет фактическое время при изменении смещения зоны. Относительная метка времени изменяется вместе со смещением часового пояса календаря.

Joda-Time и API времени даты Java 8 просто создают java.util.Календарь устарел, поэтому вам больше не нужно использовать этот причудливый API.

Joda-Time || и API времени даты Java 8 просто создают || java.util.Календарь || устарел, поэтому вам больше не нужно использовать этот причудливый API.

Joda-Time стремится исправить устаревший API даты и времени, предлагая:

С Joda-Time именно так выглядит наш предыдущий тестовый случай:

@Test
public void testTimeZonesWithDateTime() throws ParseException {
    assertEquals(
        0L, 
        newDateTimeMillis("GMT").toDate().getTime()
    );
    
    assertEquals(
        TimeUnit.HOURS.toMillis(-9), 
        newDateTimeMillis("Japan").toDate().getTime()
    );
    
    assertEquals(
        TimeUnit.HOURS.toMillis(10), 
        newDateTimeMillis("Pacific/Honolulu").toDate().getTime()
    );
    
    DateTime epoch = newDateTimeMillis("GMT");
    
    assertEquals(
        "1970-01-01T00:00:00.000Z", 
        epoch.toString()
    );
    epoch = epoch.toDateTime(DateTimeZone.forID("Japan"));
    
    assertEquals(
        0, 
        epoch.toDate().getTime()
    );
    
    assertEquals(
        "1970-01-01T09:00:00.000+09:00", 
        epoch.toString()
    );
    
    MutableDateTime mutableDateTime = epoch.toMutableDateTime();
    mutableDateTime.setChronology(
        ISOChronology.getInstance().withZone(DateTimeZone.forID("Japan"))
    );
    assertEquals(
        "1970-01-01T09:00:00.000+09:00",
        epoch.toString()
    );
}


private DateTime newDateTimeMillis(
        String timeZoneId) {
    return new DateTime(DateTimeZone.forID(timeZoneId))
            .withYear(1970)
            .withMonthOfYear(1)
            .withDayOfMonth(1)
            .withTimeAtStartOfDay();
}

API Дата и время fluent намного проще в использовании, чем java.util.Календарь#установлен . Дата-время неизменяемо, но мы можем легко переключиться на MutableDateTime , если это подходит для нашего текущего варианта использования.

По сравнению с нашим тестовым случаем Календаря, при изменении часового пояса относительная метка времени ничуть не меняется, поэтому остается той же исходной точкой во времени.

Меняется только восприятие времени человеком ( 1970-01-01T00:00:00.000 Z и 1970-01-01T09:00:00.000+09:00 указывая на то же самое абсолютное время).

Относительные и абсолютные экземпляры времени

При поддержке часовых поясов у вас в основном есть две основные альтернативы: относительная метка времени и абсолютное время.

Относительная метка времени

Числовое представление метки времени (количество миллисекунд с момента эпохи) является относительной информацией. Это значение соответствует эпохе UTC, но вам все равно нужен часовой пояс, чтобы правильно отображать фактическое время в определенном регионе.

Будучи длительным значением, это наиболее компактное представление времени, и оно идеально подходит для обмена огромными объемами данных.

Если вы не знаете исходный часовой пояс события, вы рискуете отобразить метку времени по сравнению с текущим местным часовым поясом, а это не всегда желательно.

Абсолютная метка времени

Абсолютная метка времени содержит как относительное время, так и информацию о часовом поясе. Довольно часто метки времени выражаются в их строковом представлении ISO 8601.

По сравнению с числовой формой (длиной 64 бита) строковое представление менее компактно и может занимать до 25 символов (200 бит в кодировке UTF-8).

ISO 8601 довольно часто встречается в XML-файлах, поскольку схема XML использует лексический формат, вдохновленный стандартом ISO 8601 .

Абсолютное представление времени намного удобнее, когда мы хотим восстановить экземпляр времени по сравнению с исходным часовым поясом. Клиент электронной почты может захотеть отобразить дату создания электронной почты с использованием часового пояса отправителя, и это может быть достигнуто только с использованием абсолютных меток времени.

Головоломки

Следующее упражнение направлено на демонстрацию того, насколько сложно правильно обрабатывать структуру даты/времени, соответствующую стандарту ISO 8601, с помощью утилит java.text.DateFormat .

Следующее упражнение направлено на демонстрацию того, насколько сложно правильно обрабатывать структуру даты/времени, соответствующую стандарту ISO 8601, с помощью утилит || java.text.DateFormat||.

Сначала мы собираемся протестировать возможности java.text.SimpleDateFormat синтаксического анализа, используя следующую логику тестирования:

private void dateFormatParse(
        String pattern, 
        String dateTimeString, 
        long expectedNumericTimestamp) {
    try {
        Date utcDate = new SimpleDateFormat(pattern).parse(dateTimeString);
        if(expectedNumericTimestamp != utcDate.getTime()) {
            LOGGER.warn(
                "Pattern: {}, date: {} actual epoch {} while expected epoch: {}", 
                new Object[]{
                    pattern, 
                    dateTimeString, 
                    utcDate.getTime(), 
                    expectedNumericTimestamp
                }
            );
        }
    } catch (ParseException e) {
        LOGGER.warn(
            "Pattern: {}, date: {} threw {}", 
            new Object[]{
                pattern, 
                dateTimeString, 
                e.getClass().getSimpleName()
            }
        );
    }
}

Вариант использования 1

Давайте посмотрим, как различные шаблоны ISO 8601 ведут себя в отношении этого первого анализатора:

dateFormatParse(
    "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", 
    "1970-01-01T00:00:00.200Z", 
    200L
);

Что привело к следующему результату:

Pattern: 
    yyyy-MM-dd'T'HH:mm:ss.SSS'Z', 
date: 
    1970-01-01T00:00:00.200Z 
actual epoch -7199800 while expected epoch: 200

Этот шаблон не соответствует стандарту ISO 8601. Символ одинарной кавычки является escape-последовательностью, поэтому конечный символ ‘Z’ не рассматривается как директива времени (например, время Зулу). После анализа мы просто получим ссылку на дату местного часового пояса.

Этот тест был выполнен с использованием моего текущего системного часового пояса по умолчанию Европа/Афины , который на момент написания этого поста на два часа опережает UTC.

Вариант использования 2

Согласно java.util.SimpleDateFormat документации, следующий шаблон: гггг-ММ-дд’т’Ч:мм:сс.SSSZ должен соответствовать строковому значению даты/времени ISO 8601:

dateFormatParse(
    "yyyy-MM-dd'T'HH:mm:ss.SSSZ", 
    "1970-01-01T00:00:00.200Z", 
    200L
);

Но вместо этого мы получили следующее исключение:

Pattern: 
    yyyy-MM-dd'T'HH:mm:ss.SSSZ, 
date: 
    1970-01-01T00:00:00.200Z 
threw ParseException

Таким образом, этот шаблон, похоже, не анализирует строковые значения UTC по времени Зулу.

Вариант использования 3

Следующие шаблоны отлично подходят для явных смещений:

dateFormatParse(
    "yyyy-MM-dd'T'HH:mm:ss.SSSZ", 
    "1970-01-01T00:00:00.200+0000", 
    200L
);

Вариант использования 4

Этот шаблон также совместим с другими смещениями часовых поясов:

dateFormatParse(
    "yyyy-MM-dd'T'HH:mm:ss.SSSZ", 
    "1970-01-01T00:00:00.200+0100", 
    200L - 1000 * 60 * 60
);

Пример использования 5

Чтобы соответствовать обозначению времени зулу, нам нужно использовать следующий шаблон:

dateFormatParse(
    "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", 
    "1970-01-01T00:00:00.200Z", 
    200L
);

Пример использования 6

К сожалению, этот последний шаблон несовместим с явными смещениями часовых поясов:

dateFormatParse(
    "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", 
    "1970-01-01T00:00:00.200+0000", 
    200L
);

В конечном итоге со следующим исключением:

Pattern: 
    yyyy-MM-dd'T'HH:mm:ss.SSSXXX,
date: 
    1970-01-01T00:00:00.200+0000 
threw ParseException

В конечном итоге со следующим исключением:

В отличие от java.text.SimpleDateFormat , Joda-Time совместим с любым шаблоном ISO 8601. Следующий тестовый случай будет использоваться для предстоящих тестовых случаев:

private void jodaTimeParse(
        String dateTimeString, 
        long expectedNumericTimestamp) {
    Date utcDate = DateTime.parse(dateTimeString).toDate();
    if(expectedNumericTimestamp != utcDate.getTime()) {
        LOGGER.warn(
            "date: {} actual epoch {} while expected epoch: {}", 
            new Object[]{
                dateTimeString, 
                utcDate.getTime(), 
                expectedNumericTimestamp
            }
        );
    }
}

Joda-Time совместим со всеми стандартными форматами даты и времени ISO 8601:

jodaTimeParse(
    "1970-01-01T00:00:00.200Z", 
    200L
);

jodaTimeParse(
    "1970-01-01T00:00:00.200+0000", 
    200L
);

jodaTimeParse(
    "1970-01-01T00:00:00.200+0100", 
    200L - 1000 * 60 * 60
);

Вывод

Как вы можете видеть, с древними утилитами даты и времени Java работать непросто. Joda-Time-гораздо лучшая альтернатива, предлагающая лучшие функции управления временем.

Если вам посчастливилось работать с Java 8, стоит переключиться на Java 8 Date/Time API , который очень сильно основан на Joda-Времени .

Код доступен на GitHub .