12.3 处理不同的时区和历法
之前你看到的日期和时间的种类都不包含时区信息。时区的处理是新版日期和时间API新增加的重要功能,使用新版日期和时间API时区的处理被极大地简化了。新版java.time.ZoneId类是老版java.util.TimeZone类的替代品。它的设计目标就是要让你无须为时区处理的复杂和烦琐而操心,比如处理夏令时(daylight saving time,DST)这种问题。跟其他日期和时间API类一样,ZoneId类也是无法修改的。
12.3.1 使用时区
时区是按照一定的规则将区域划分成的标准时间相同的区间。在ZoneRules这个类中包含了40个这样的实例。你可以简单地通过调用ZoneId的getRules()得到指定时区的规则。每个特定的ZoneId对象都由一个地区ID标识,比如:
ZoneId romeZone = ZoneId.of("Europe/Rome");
地区ID都为“{区域}/{城市}”的格式,这些地区集合的设定都由因特网编号分配机构(IANA)的时区数据库提供。你可以通过Java 8的新方法toZoneId将一个老的时区对象转换为ZoneId:
ZoneId zoneId = TimeZone.getDefault().toZoneId();
一旦得到一个ZoneId对象,你就可以将它与LocalDate、LocalDateTime或者是Instant对象整合起来,构造为一个ZonedDateTime实例,它代表了相对于指定时区的时间点,代码清单如下。
代码清单 12-13 为时间点添加时区信息
LocalDate date = LocalDate.of(2014, Month.MARCH, 18);ZonedDateTime zdt1 = date.atStartOfDay(romeZone);LocalDateTime dateTime = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45);ZonedDateTime zdt2 = dateTime.atZone(romeZone);Instant instant = Instant.now();ZonedDateTime zdt3 = instant.atZone(romeZone);
图12-1对ZonedDateTime的组成部分进行了说明,相信能够帮助你理解LocaleDate、LocalTime、LocalDateTime以及ZoneId之间的差异。

图 12-1 理解ZonedDateTime
通过ZoneId,你还可以将LocalDateTime转换为Instant:
LocalDateTime dateTime = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45);Instant instantFromDateTime = dateTime.toInstant(romeZone);
你也可以通过反向的方式得到LocalDateTime对象:
Instant instant = Instant.now();LocalDateTime timeFromInstant = LocalDateTime.ofInstant(instant, romeZone);
注意,采用Instant非常有帮助,因为你经常需要处理很可能还在使用Date类的遗留代码。Instant中新增的两个方法能帮助你在弃用API跟新的日期和时间API之间执行互操作,这两个方法分别是:toInstant()和静态方法fromInstant()。
12.3.2 利用和UTC/格林尼治时间的固定偏差计算时区
另一种比较通用的表达时区的方式是利用当前时区和UTC/格林尼治的固定偏差。比如,基于这个理论,你可以说“纽约落后于伦敦5小时”。这种情况下,你可以使用ZoneOffset类,它是ZoneId的一个子类,表示的是当前时间和伦敦格林尼治子午线时间的差异:
ZoneOffset newYorkOffset = ZoneOffset.of("-05:00");
“–05:00”的偏差实际上对应的是美国东部标准时间。注意,使用这种方式定义的ZoneOffset并未考虑任何夏令时的影响,所以在大多数情况下,不推荐使用。因为ZoneOffset也是ZoneId,所以你可以像代码清单12-13那样使用它。你甚至还可以创建这样的OffsetDateTime,它使用ISO-8601的历法系统,以相对于UTC/格林尼治时间的偏差方式表示日期时间。
LocalDateTime dateTime = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45);OffsetDateTime dateTimeInNewYork = OffsetDateTime.of(dateTime, newYorkOffset);
新版的日期和时间API还提供了另一个高级特性,即对非ISO历法系统(non-ISO calendaring system)的支持。
12.3.3 使用别的日历系统
ISO-8601日历系统是世界文明日历系统的事实标准。但是,Java 8中另外还提供了四种其他的日历系统。这些日历系统中的每一个都有一个对应的日期类,分别是ThaiBuddhistDate、MinguoDate、JapaneseDate以及HijrahDate。所有这些类以及LocalDate都实现了ChronoLocalDate接口,能够对公历的日期进行建模。利用LocalDate对象,你可以创建这些类的实例。更通用地说,使用它们提供的静态工厂方法,你可以创建任何一个Temporal对象的实例,如下所示:
LocalDate date = LocalDate.of(2014, Month.MARCH, 18);JapaneseDate japaneseDate = JapaneseDate.from(date);
或者,你还可以为某个Locale显式地创建日历系统,接着创建该Locale对应的日期的实例。新的日期和时间API中,Chronology接口建模了一个日历系统,使用它的静态工厂方法ofLocale,可以得到它的一个实例,代码如下:
Chronology japaneseChronology = Chronology.ofLocale(Locale.JAPAN);ChronoLocalDate now = japaneseChronology.dateNow();
日期和时间API的设计者建议我们使用LocalDate,尽量避免使用ChronoLocalDate,原因是开发者在他们的代码中可能会做一些假设,而这些假设在不同的日历系统中,有可能不成立。比如,有人可能会做这样的假设,即一个月天数不会超过31天,一年包括12个月,或者一年中包含的月份数目是固定的。由于这些原因,建议你尽量在你的应用中使用LocalDate,包括存储、操作、业务规则的解读;不过如果你需要将程序的输入或者输出本地化,那么应该使用ChronoLocalDate类。
伊斯兰教日历
在Java 8新添加的几种日历类型中,HijrahDate(伊斯兰教日历)是最复杂的一个,因为它会发生各种变化。Hijrah日历系统构建于农历月份继承之上。Java 8提供了多种方法判断一个月份,比如新月,在世界的哪些地方可见,或者说它只能首先可见于沙特阿拉伯。withVariant方法可以用于选择期望的变化。为了支持HijrahDate这一标准,Java 8中还包括了乌姆库拉(Umm Al-Qura)变量。
下面这段代码作为一个例子说明了如何在ISO日历中计算当前伊斯兰年中斋月的起始和终止日期:
HijrahDate ramadanDate =HijrahDate.now().with(ChronoField.DAY_OF_MONTH, 1).with(ChronoField.MONTH_OF_YEAR, 9); ←---- 取得当前的Hijrah日期,紧接着对其进行修正,得到斋月的第一天,即第9个月System.out.println("Ramadan starts on " +IsoChronology.INSTANCE.date(ramadanDate) + ←---- IsoChronology.INSTANCE是IsoChronology类的一个静态实例" and ends on " +IsoChronology.INSTANCE.date( ←---- 斋月1438始于2017-05-26,止于2017-06-24ramadanDate.with(TemporalAdjusters.lastDayOfMonth())));
