java 统一处理时区_Java时区处理初学者指南

本文介绍了Java中处理时区的基本概念,包括Unix纪元、相对和绝对时间戳、时区转换,并对比了`java.util.Date`、`java.util.Calendar`与Joda-Time库在处理时间上的差异。特别强调了ISO 8601日期/时间标准在Java中的应用,通过多个用例展示了`java.text.SimpleDateFormat`的局限性和Joda-Time的兼容性优势。
摘要由CSDN通过智能技术生成

java 统一处理时区

基本时间观念

大多数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:

使用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.000Z1970-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启发很大。

翻译自: https://www.javacodegeeks.com/2014/11/a-beginners-guide-to-java-time-zone-handling.html

java 统一处理时区

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值