基本时间观念
大多数Web应用程序必须支持不同的时区,而正确处理时区绝非易事。 更糟糕的是,您必须确保各种编程语言(例如,前端Javascript,中间件中的Java和作为数据存储库的MongoDB)之间的时间戳是一致的。 这篇文章旨在解释绝对时间和相对时间的基本概念。
时代
纪元是绝对时间基准。 大多数编程语言(例如Java,Javascript,Python)使用Unix纪元(1970年1月1日午夜)来表示给定的时间戳,即自固定时间点引用以来经过的毫秒数。
相对数字时间戳
相对数字时间戳表示为从纪元以来经过的毫秒数。
时区
协调世界时(UTC)是最常见的时间标准。 UTC时区(相当于GMT )表示所有其他时区涉及的时间参考(通过正/负偏移量)。
UTC时区通常称为Zulu时间(Z)或UTC + 0。 日本时区为UTC + 9,而檀香山时区为UTC-10。 在Unix时代(1970年1月1日UTC时区),东京为1970年1月1日,檀香山为1969年12月31日14:00。
ISO 8601
ISO 8601是最广泛的日期/时间表示标准,它使用以下日期/时间格式:
时区 | 符号 |
---|---|
世界标准时间 | 1970-01-01T00:00:00.000 + 00:00 |
UTC祖鲁时间 | 1970-01-01T00:00:00.000 + Z |
时雄 | 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类之一,我们可以在以下示例中进行演示:
@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时区),东京时间提前了9个小时,而檀香山却落后了10个小时。
更改日历时区会在偏移时区偏移时保留实际时间。 相对时间戳随日历时区偏移量而变化。
Joda-Time和Java 8 Date Time API只是使java.util.Calandar过时,因此您不必再使用此古怪的API。
org.joda.time.DateTime
Joda-Time旨在通过提供以下服务来修复旧版Date / Time API:
- 不变和可变的日期结构
- 流利的API
- 更好地支持ISO 8601标准
使用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();
}
DateTime流利的API比java.util.Calendar#set易于使用。 DateTime是不可变的,但如果适合当前的用例,我们可以轻松地切换到MutableDateTime 。
与我们的Calendar测试用例相比,当更改时区时,相对时间戳不会改变,因此保留了相同的原始时间点。
只是人类的时间感知发生了变化( 1970-01-01T00:00:00.000Z和1970-01-01T09:00:00.000 + 09:00指向相同的绝对时间)。
相对时间与绝对时间实例
当支持时区时,基本上有两个主要选择:相对时间戳和绝对时间信息。
相对时间戳
时间戳的数字表示形式(自纪元以来的毫秒数)是相对信息。 该值是针对UTC时代给出的,但是您仍然需要一个时区来正确表示特定区域上的实际时间。
作为一个长值,它是最紧凑的时间表示形式,是交换大量数据时的理想选择。
如果您不知道原始事件的时区,则可能会显示与当前本地时区相对的时间戳,这并不总是可取的。
绝对时间戳
绝对时间戳包含相对时间以及时区信息。 在其ISO 8601字符串表示中表示时间戳是很常见的。
与数字形式(64位长)相比,字符串表示的紧凑性较低,它最多可包含25个字符(UTF-8编码为200位)。
ISO 8601在XML文件中非常常见,因为XML模式使用的是受ISO 8601标准启发的词汇格式 。
当我们想针对原始时区重构时间实例时,绝对时间表示会更加方便。 电子邮件客户端可能希望使用发件人的时区显示电子邮件创建日期,而这只能使用绝对时间戳来实现。
谜题
以下练习旨在说明使用古老的java.text.DateFormat实用程序正确处理符合ISO 8601的日期/时间结构有多么困难。
java.text.SimpleDateFormat
首先,我们将使用以下测试逻辑来测试java.text.SimpleDateFormat解析功能:
/*** 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模式如何针对第一个解析器表现:
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。 单引号字符是一个转义序列,因此最后的“ Z”符号不会被视为时间指令(例如Zulu时间)。 解析后,我们将仅获取本地时区的Date参考。
该测试是使用我当前的系统默认欧洲/雅典时区运行的,截至撰写本文时,它比UTC提前两个小时。
用例2
根据java.util.SimpleDateFormat文档,以下模式: yyyy-MM-dd'T'HH:mm:ss.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
因此,此模式似乎无法解析Zulu时间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
org.joda.time.DateTime
与java.text.SimpleDateFormat相反, Joda-Time与任何ISO 8601模式兼容。 以下测试用例将用于即将推出的测试用例:
/*** 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日期/时间格式兼容:
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,则值得切换到Java 8 Date / Time API ,该API是从头开始设计的,但深受Joda-Time启发 。
- 代码可在GitHub上获得 。
翻译自: https://www.javacodegeeks.com/2014/11/a-beginners-guide-to-java-time-zone-handling.html