Основные понятия времени
Большинство веб-приложений должны поддерживать разные часовые пояса, и правильная обработка часовых поясов не так проста. Что еще хуже, вы должны убедиться, что временные метки согласованы для разных языков программирования (например, 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 |
Tokio | 1970-01-01T00: 00: 00,000 + 09: 00 |
Гонолулу | 1969-12-31T14: 00: 00.000-10: 00 |
Основы времени Java
java.util.Date
java.util.Date — определенно самый распространенный класс, относящийся ко времени. Он представляет собой фиксированный момент времени, выраженный как относительное количество миллисекунд, прошедших с начала эпохи. java.util.Date не зависит от часового пояса , за исключением метода toString, который использует локальный часовой пояс для генерации представления String.
java.util.Calendar
Java.util.Calendar является одновременно фабрикой даты / времени, а также экземпляром синхронизации с учетом часового пояса. Это один из наименее удобных для работы классов Java API, и мы можем продемонстрировать это в следующем примере:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
@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 newCalendarInstance(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 и Java 8 Date Time API просто делают java.util.Calandar устаревшим, так что вам больше не придется использовать этот причудливый API.
org.joda.time.DateTime
Joda-Time стремится исправить устаревший API Date / Time, предлагая:
- как неизменяемые, так и изменяемые структуры дат
- свободный API
- лучшая поддержка стандарта ISO 8601
С Joda-Time так выглядит наш предыдущий тестовый пример:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
@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 DateTime намного проще в использовании, чем набор java.util.Calendar # . DateTime является неизменным, но мы можем легко переключиться на MutableDateTime, если это подходит для нашего текущего варианта использования.
По сравнению с нашим тестовым календарем, при изменении часового пояса относительная временная метка не меняется, поэтому остается прежней исходной точкой времени.
Меняется только восприятие человеческого времени ( 1970-01-01T00: 00: 00.000Z и 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 .
java.text.SimpleDateFormat
Сначала мы протестируем возможности разбора java.text.SimpleDateFormat , используя следующую логику тестирования:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
/** * DateFormat parsing utility * @param pattern date/time pattern * @param dateTimeString date/time string value * @param expectedNumericTimestamp expected millis since epoch */ 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 ведут себя с этим первым анализатором:
1
|
dateFormatParse( "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" , "1970-01-01T00:00:00.200Z" , 200L); |
Дает следующий результат:
1
|
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 следующий шаблон: гггг-ММ-дд’Т’ХЧ: мм: сс.СССЗ должен соответствовать строковому значению даты / времени ISO 8601:
1
|
dateFormatParse( "yyyy-MM-dd'T'HH:mm:ss.SSSZ" , "1970-01-01T00:00:00.200Z" , 200L); |
Но вместо этого мы получили следующее исключение:
1
|
Pattern: yyyy-MM- dd 'T' HH:mm:ss.SSSZ, date : 1970-01-01T00:00:00.200Z threw ParseException |
Таким образом, этот шаблон, кажется, не анализирует строковые значения UTC времени Зулу.
Вариант использования 3
Следующие шаблоны прекрасно работают для явных смещений:
1
|
dateFormatParse( "yyyy-MM-dd'T'HH:mm:ss.SSSZ" , "1970-01-01T00:00:00.200+0000" , 200L); |
Вариант использования 4
Этот шаблон также совместим с другими смещениями часового пояса:
1
|
dateFormatParse( "yyyy-MM-dd'T'HH:mm:ss.SSSZ" , "1970-01-01T00:00:00.200+0100" , 200L - 1000 * 60 * 60 ); |
Вариант использования 5
Чтобы соответствовать нотации времени зулу, нам нужно использовать следующую схему:
1
|
dateFormatParse( "yyyy-MM-dd'T'HH:mm:ss.SSSXXX" , "1970-01-01T00:00:00.200Z" , 200L); |
Вариант использования 6
К сожалению, этот последний шаблон не совместим с явными смещениями часовых поясов:
1
|
dateFormatParse( "yyyy-MM-dd'T'HH:mm:ss.SSSXXX" , "1970-01-01T00:00:00.200+0000" , 200L); |
Окончание со следующим исключением:
1
|
Pattern: yyyy-MM- dd 'T' HH:mm:ss.SSSXXX, date : 1970-01-01T00:00:00.200+0000 threw ParseException |
org.joda.time.DateTime
В отличие от java.text.SimpleDateFormat , Joda-Time совместим с любым шаблоном ISO 8601. Следующий тестовый пример будет использоваться для следующих тестовых случаев:
01
02
03
04
05
06
07
08
09
10
11
|
/** * Joda-Time parsing utility * @param dateTimeString date/time string value * @param expectedNumericTimestamp expected millis since epoch */ 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:
1
2
3
|
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 Date / Time работать нелегко. Joda-Time — намного лучшая альтернатива, предлагающая лучшие функции обработки времени.
Если вам доведется работать с Java 8, стоит перейти на API даты / времени Java 8 , разработанный с нуля, но очень вдохновленный Joda-Time .
- Код доступен на GitHub .
Ссылка: | Руководство для новичков по обработке часовых поясов Java от нашего партнера JCG Влада Михалча в блоге Влада Михалча . |