66 使用 UTC 和 GMT 获取所有时区
UTC 和 GMT 被认为是处理日期和时间的标准参考。今天,UTC 是首选的方法,但是 UTC 和 GMT 在大多数情况下应该返回相同的结果。
为了获得 UTC 和 GMT 的所有时区,解决方案应该关注 JDK8 前后的实现。所以,让我们从 JDK8 之前有用的解决方案开始。
JDK8 之前
解决方案需要提取可用的时区 ID(非洲/巴马科、欧洲/贝尔格莱德等)。此外,每个时区 ID 都应该用来创建一个TimeZone
对象。最后,解决方案需要提取特定于每个时区的偏移量,并考虑到夏令时。绑定到本书的代码包含此解决方案。
从 JDK8 开始
新的 Java 日期时间 API 为解决这个问题提供了新的工具。
在第一步,可用的时区 id 可以通过ZoneId
类获得,如下所示:
Set<String> zoneIds = ZoneId.getAvailableZoneIds();
在第二步,每个时区 ID 都应该用来创建一个ZoneId
实例。这可以通过ZoneId.of(String zoneId)
方法实现:
ZoneId zoneid = ZoneId.of(current_zone_Id);
在第三步,每个ZoneId可用于获得特定于所识别区域的时间。这意味着需要一个“实验室老鼠”参考日期时间。此参考日期时间(无时区,LocalDateTime.now())通过LocalDateTime.atZone()与给定时区(ZoneId)组合,以获得ZoneDateTime(可识别时区的日期时间):
LocalDateTime now = LocalDateTime.now(); ZonedDateTime zdt = now.atZone(ZoneId.of(zone_id_instance));
atZone()
方法尽可能地匹配日期时间,同时考虑时区规则,例如夏令时。
在第四步,代码可以利用ZonedDateTime
来提取 UTC 偏移量(例如,对于欧洲/布加勒斯特,UTC 偏移量为+02:00
):
String utcOffset = zdt.getOffset().getId().replace("Z", "+00:00");
getId()
方法返回规范化区域偏移 ID,+00:00
偏移作为Z
字符返回;因此代码需要快速将Z
替换为+00:00
,以便与其他偏移对齐,这些偏移遵循+hh:mm
或+hh:mm:ss
格式。
现在,让我们将这些步骤合并到一个辅助方法中:
public static List<String> fetchTimeZones(OffsetType type) { List<String> timezones = new ArrayList<>(); Set<String> zoneIds = ZoneId.getAvailableZoneIds(); LocalDateTime now = LocalDateTime.now(); zoneIds.forEach((zoneId) -> { timezones.add("(" + type + now.atZone(ZoneId.of(zoneId)) .getOffset().getId().replace("Z", "+00:00") + ") " + zoneId); }); return timezones; }
假设此方法存在于DateTimes
类中,则获得以下代码:
List<String> timezones = DateTimes.fetchTimeZones(DateTimes.OffsetType.GMT); Collections.sort(timezones); // optional sort timezones.forEach(System.out::println);
此外,还显示了一个输出快照,如下所示:
(GMT+00:00) Africa/Abidjan (GMT+00:00) Africa/Accra (GMT+00:00) Africa/Bamako ... (GMT+11:00) Australia/Tasmania (GMT+11:00) Australia/Victoria ...
67 获取所有可用时区中的本地日期时间
可通过以下步骤获得此问题的解决方案:
- 获取本地日期和时间。
- 获取可用时区。
- 在 JDK8 之前,使用
SimpleDateFormat
和setTimeZone()
方法。 - 从 JDK8 开始,使用
ZonedDateTime
。
JDK8 之前
在 JDK8 之前,获取当前本地日期时间的快速解决方案是调用Date
空构造器。此外,还可以使用Date
在所有可用的时区中显示,这些时区可以通过TimeZone
类获得。绑定到本书的代码包含此解决方案。
从 JDK8 开始
从 JDK8 开始,获取默认时区中当前本地日期时间的一个方便解决方案是调用ZonedDateTime.now()
方法:
ZonedDateTime zlt = ZonedDateTime.now();
最后,代码可以循环zoneIds
,对于每个区域 ID,可以调用ZonedDateTime.withZoneSameInstant(ZoneId zone)
方法。此方法返回具有不同时区的此日期时间的副本,并保留以下瞬间:
public static List<String> localTimeToAllTimeZones() { List<String> result = new ArrayList<>(); Set<String> zoneIds = ZoneId.getAvailableZoneIds(); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MMM-dd'T'HH:mm:ss a Z"); ZonedDateTime zlt = ZonedDateTime.now(); zoneIds.forEach((zoneId) -> { result.add(zlt.format(formatter) + " in " + zoneId + " is " + zlt.withZoneSameInstant(ZoneId.of(zoneId)) .format(formatter)); }); return result; }
此方法的输出快照可以如下所示:
2019-Feb-26T14:26:30 PM +0200 in Africa/Nairobi is 2019-Feb-26T15:26:30 PM +0300 2019-Feb-26T14:26:30 PM +0200 in America/Marigot is 2019-Feb-26T08:26:30 AM -0400 ... 2019-Feb-26T14:26:30 PM +0200 in Pacific/Samoa is 2019-Feb-26T01:26:30 AM -1100
68 显示航班的日期时间信息
本节提供的解决方案将显示有关从澳大利亚珀斯到欧洲布加勒斯特的 15 小时 30 分钟航班的以下信息:
- UTC 出发和到达日期时间
- 离开珀斯的日期时间和到达布加勒斯特的日期时间
- 离开和到达布加勒斯特的日期时间
假设从珀斯出发的参考日期时间为 2019 年 2 月 26 日 16:00(或下午 4:00):
LocalDateTime ldt = LocalDateTime.of( 2019, Month.FEBRUARY, 26, 16, 00);
首先,让我们将这个日期时间与澳大利亚/珀斯(+08:00)的时区结合起来。这将产生一个特定于澳大利亚/珀斯的ZonedDateTime
对象(这是出发时珀斯的时钟日期和时间):
// 04:00 PM, Feb 26, 2019 +0800 Australia/Perth ZonedDateTime auPerthDepart = ldt.atZone(ZoneId.of("Australia/Perth"));
此外,让我们在ZonedDateTime
中加上 15 小时 30 分钟。结果ZonedDateTime
表示珀斯的日期时间(这是抵达布加勒斯特时珀斯的时钟日期和时间):
// 07:30 AM, Feb 27, 2019 +0800 Australia/Perth ZonedDateTime auPerthArrive = auPerthDepart.plusHours(15).plusMinutes(30);
现在,让我们计算一下布加勒斯特的日期时间和珀斯的出发日期时间。基本上,以下代码表示从布加勒斯特时区的珀斯时区出发的日期和时间:
// 10:00 AM, Feb 26, 2019 +0200 Europe/Bucharest ZonedDateTime euBucharestDepart = auPerthDepart.withZoneSameInstant(ZoneId.of("Europe/Bucharest"));
最后,让我们计算一下到达布加勒斯特的日期和时间。以下代码表示布加勒斯特时区珀斯时区的到达日期时间:
// 01:30 AM, Feb 27, 2019 +0200 Europe/Bucharest ZonedDateTime euBucharestArrive = auPerthArrive.withZoneSameInstant(ZoneId.of("Europe/Bucharest"));
如下图所示,从珀斯出发的 UTC 时间是上午 8:00,而到达布加勒斯特的 UTC 时间是晚上 11:30:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rdxM6AQc-1657077517351)(img/09ecaf27-f809-42b1-8858-ecf44aa33d5f.png)]
这些时间可以很容易地提取为OffsetDateTime,如下所示:
// 08:00 AM, Feb 26, 2019 OffsetDateTime utcAtDepart = auPerthDepart.withZoneSameInstant( ZoneId.of("UTC")).toOffsetDateTime(); // 11:30 PM, Feb 26, 2019 OffsetDateTime utcAtArrive = auPerthArrive.withZoneSameInstant( ZoneId.of("UTC")).toOffsetDateTime();
69 将 Unix 时间戳转换为日期时间
对于这个解决方案,假设下面的 Unix 时间戳是 1573768800。此时间戳等效于以下内容:
11/14/2019 @ 10:00pm (UTC)
ISO-8601 中的2019-11-14T22:00:00+00:00
Thu, 14 Nov 2019 22:00:00 +0000,RFC 822、1036、1123、2822
Thursday, 14-Nov-19 22:00:00 UTC,RFC 2822
2019-11-14T22:00:00+00:00在 RFC 3339 中
为了将 Unix 时间戳转换为日期时间,必须知道 Unix 时间戳的分辨率以秒为单位,而java.util.Date需要毫秒。因此,从 Unix 时间戳获取Date对象的解决方案需要将 Unix 时间戳乘以 1000,从秒转换为毫秒,如下两个示例所示:
long unixTimestamp = 1573768800; // Fri Nov 15 00:00:00 EET 2019 - in the default time zone Date date = new Date(unixTimestamp * 1000L); // Fri Nov 15 00:00:00 EET 2019 - in the default time zone Date date = new Date(TimeUnit.MILLISECONDS .convert(unixTimestamp, TimeUnit.SECONDS));
从 JDK8 开始,Date
类使用from(Instant instant)
方法。此外,Instant
类附带了ofEpochSecond(long epochSecond)
方法,该方法使用1970-01-01T00:00:00Z
的纪元的给定秒数返回Instant
的实例:
// 2019-11-14T22:00:00Z in UTC Instant instant = Instant.ofEpochSecond(unixTimestamp); // Fri Nov 15 00:00:00 EET 2019 - in the default time zone Date date = Date.from(instant);
上一示例中获得的瞬间可用于创建LocalDateTime
或ZonedDateTime
,如下所示:
// 2019-11-15T06:00 LocalDateTime date = LocalDateTime .ofInstant(instant, ZoneId.of("Australia/Perth")); // 2019-Nov-15 00:00:00 +0200 Europe/Bucharest ZonedDateTime date = ZonedDateTime .ofInstant(instant, ZoneId.of("Europe/Bucharest"));
70 查找每月的第一天/最后一天
这个问题的正确解决将依赖于 JDK8、Temporal
和TemporalAdjuster
接口。
Temporal接口位于日期和时间的表示后面。换句话说,表示日期和/或时间的类实现了这个接口。例如,以下类只是实现此接口的几个类:
LocalDate(ISO-8601 日历系统中没有时区的日期)
LocalTime(ISO-8601 日历系统中无时区的时间)
LocalDateTime(ISO-8601 日历系统中无时区的日期时间)
ZonedDateTime(ISO-8601 日历系统中带时区的日期时间),依此类推
OffsetDateTime(在 ISO-8601 日历系统中,从 UTC/格林威治时间偏移的日期时间)
HijrahDate(希吉拉历法系统中的日期)
TemporalAdjuster类是一个函数式接口,它定义了可用于调整Temporal对象的策略。除了可以定义自定义策略外,TemporalAdjuster类还提供了几个预定义的策略,如下所示(文档包含了整个列表,非常令人印象深刻):
firstDayOfMonth()(返回当月第一天)
lastDayOfMonth()(返回当月最后一天)
firstDayOfNextMonth()(次月 1 日返回)
firstDayOfNextYear()(次年第一天返回)
注意,前面列表中的前两个调整器正是这个问题所需要的。
考虑一个修正-LocalDate:
LocalDate date = LocalDate.of(2019, Month.FEBRUARY, 27);
让我们看看二月的第一天/最后一天是什么时候:
// 2019-02-01 LocalDate firstDayOfFeb = date.with(TemporalAdjusters.firstDayOfMonth()); // 2019-02-28 LocalDate lastDayOfFeb = date.with(TemporalAdjusters.lastDayOfMonth());
看起来依赖预定义的策略非常简单。但是,假设问题要求您查找 2019 年 2 月 27 日之后的 21 天,也就是 2019 年 3 月 20 日。对于这个问题,没有预定义的策略,因此需要自定义策略。此问题的解决方案可以依赖 Lambda 表达式,如以下辅助方法中所示:
public static LocalDate getDayAfterDays( LocalDate startDate, int days) { Period period = Period.ofDays(days); TemporalAdjuster ta = p -> p.plus(period); LocalDate endDate = startDate.with(ta); return endDate; }
如果此方法存在于名为DateTimes
的类中,则以下调用将返回预期结果:
// 2019-03-20 LocalDate datePlus21Days = DateTimes.getDayAfterDays(date, 21);
遵循相同的技术,但依赖于static
工厂方法ofDateAdjuster()
,下面的代码片段定义了一个静态调整器,返回下一个星期六的日期:
static TemporalAdjuster NEXT_SATURDAY = TemporalAdjusters.ofDateAdjuster(today -> { DayOfWeek dayOfWeek = today.getDayOfWeek(); if (dayOfWeek == DayOfWeek.SATURDAY) { return today; } if (dayOfWeek == DayOfWeek.SUNDAY) { return today.plusDays(6); } return today.plusDays(6 - dayOfWeek.getValue()); });
我们将此方法称为 2019 年 2 月 27 日(下一个星期六是 2019 年 3 月 2 日):
// 2019-03-02 LocalDate nextSaturday = date.with(NEXT_SATURDAY);
最后,这个函数式接口定义了一个名为adjustInto()
的abstract
方法。在自定义实现中,可以通过向该方法传递一个Temporal
对象来覆盖该方法,如下所示:
public class NextSaturdayAdjuster implements TemporalAdjuster { @Override public Temporal adjustInto(Temporal temporal) { DayOfWeek dayOfWeek = DayOfWeek .of(temporal.get(ChronoField.DAY_OF_WEEK)); if (dayOfWeek == DayOfWeek.SATURDAY) { return temporal; } if (dayOfWeek == DayOfWeek.SUNDAY) { return temporal.plus(6, ChronoUnit.DAYS); } return temporal.plus(6 - dayOfWeek.getValue(), ChronoUnit.DAYS); } }
下面是用法示例:
NextSaturdayAdjuster nsa = new NextSaturdayAdjuster(); // 2019-03-02 LocalDate nextSaturday = date.with(nsa);
71 定义/提取区域偏移
通过区域偏移,我们了解需要从 GMT/UTC 时间中添加/减去的时间量,以便获得全球特定区域(例如,澳大利亚珀斯)的日期时间。通常,区域偏移以固定的小时和分钟数打印:
+02:00
、-08:30
、+0400
、UTC+01:00
,依此类推。
因此,简而言之,时区偏移量是指时区与 GMT/UTC 之间的时间差。
JDK8 之前
在 JDK8 之前,可以通过java.util.TimeZone
定义一个时区,有了这个时区,代码就可以通过TimeZone.getRawOffset()
方法得到时区偏移量(原始部分来源于这个方法不考虑夏令时)。绑定到本书的代码包含此解决方案。
从 JDK8 开始
从 JDK8 开始,有两个类负责处理时区表示。首先是java.time.ZoneId,表示欧洲雅典等时区;其次是java.time.ZoneOffset(扩展ZoneId),表示指定时区的固定时间(偏移量),以 GMT/UTC 表示。
新的 Java 日期时间 API 默认处理夏令时;因此,使用夏令时的夏-冬周期区域将有两个ZoneOffset类。
UTC 区域偏移量可以很容易地获得,如下所示(这是+00:00,在 Java 中用Z字符表示):
// Z ZoneOffset zoneOffsetUTC = ZoneOffset.UTC;
系统默认时区也可以通过ZoneOffset
类获取:
// Europe/Athens ZoneId defaultZoneId = ZoneOffset.systemDefault();
为了使用夏令时进行分区偏移,代码需要将日期时间与其关联。例如,关联一个LocalDateTime
类(也可以使用Instant
),如下所示:
// by default it deals with the Daylight Saving Times LocalDateTime ldt = LocalDateTime.of(2019, 6, 15, 0, 0); ZoneId zoneId = ZoneId.of("Europe/Bucharest"); // +03:00 ZoneOffset zoneOffset = zoneId.getRules().getOffset(ldt);
区域偏移量也可以从字符串中获得。例如,以下代码获得+02:00
的分区偏移:
ZoneOffset zoneOffsetFromString = ZoneOffset.of("+02:00");
这是一种非常方便的方法,可以将区域偏移快速添加到支持区域偏移的Temporal对象。例如,使用它将区域偏移添加到OffsetTime和OffsetDateTime(用于在数据库中存储日期或通过电线发送的方便方法):
OffsetTime offsetTime = OffsetTime.now(zoneOffsetFromString); OffsetDateTime offsetDateTime = OffsetDateTime.now(zoneOffsetFromString);
我们问题的另一个解决方法是依赖于从小时、分钟和秒来定义ZoneOffset。ZoneOffset的一个助手方法专门用于:
// +08:30 (this was obtained from 8 hours and 30 minutes) ZoneOffset zoneOffsetFromHoursMinutes = ZoneOffset.ofHoursMinutes(8, 30);
在ZoneOffset.ofHoursMinutes()旁边有ZoneOffset.ofHours()、ofHoursMinutesSeconds()和ofTotalSeconds()。
最后,每个支持区域偏移的Temporal对象都提供了一个方便的getOffset()方法。例如,下面的代码从前面的offsetDateTime对象获取区域偏移:
// +02:00 ZoneOffset zoneOffsetFromOdt = offsetDateTime.getOffset();
72 在日期和时间之间转换
这里给出的解决方案将涵盖以下Temporal
类—Instant
、LocalDate
、LocalDateTime
、ZonedDateTime
、OffsetDateTime
、LocalTime
和OffsetTime
。