目录

新的日期和时间API

simple beautiful strong immutable thread-safe

本章内容

  • 为什么在Java 8中需要引入新的日期和时间库
  • 同时为人和机器表示日期和时间
  • 定义时间的度量
  • 操纵、格式化以及解析日期
  • 处理不同的时区和历法

Java的API提供了很多有用的组件,能帮助你构建复杂的应用。不过,Java API也不总是完美 的。我们相信大多数有经验的程序员都会赞同Java 8之前的库对日期和时间的支持就非常不理想。 然而,你也不用太担心:Java 8中引入全新的日期和时间API就是要解决这一问题。

在Java 1.0中,对日期和时间的支持只能依赖java.util.Date类。正如类名所表达的,这 个类无法表示日期,只能以毫秒的精度表示时间。更糟糕的是它的易用性,由于某些原因未知的 设计决策,这个类的易用性被深深地损害了,比如:年份的起始选择是1900年,月份的起始从0 开始。这意味着,如果你想要用Date表示Java 8的发布日期,即2014年3月18日,需要创建下面 这样的Date实例:

1
Date date = new Date(114, 2, 18);

它的打印输出效果为:

1
Tue Mar 18 00:00:00 CET 2014

看起来不那么直观,不是吗?此外,甚至Date类的toString方法返回的字符串也容易误导 人。以我们的例子而言,它的返回值中甚至还包含了JVM的默认时区CET,即中欧时间(Central Europe Time)。但这并不表示Date类在任何方面支持时区。

随着Java 1.0退出历史舞台,Date类的种种问题和限制几乎一扫而光,但很明显,这些历史 旧账如果不牺牲前向兼容性是无法解决的。所以,在Java 1.1中,Date类中的很多方法被废弃了, 取而代之的是java.util.Calendar类。很不幸,Calendar类也有类似的问题和设计缺陷,导 致使用这些方法写出的代码非常容易出错。比如,月份依旧是从0开始计算(不过,至少Calendar 类拿掉了由1900年开始计算年份这一设计)。更糟的是,同时存在Date和Calendar这两个类,也增加了程序员的困惑。到底该使用哪一个类呢?此外,有的特性只在某一个类有提供,比如用 于以语言无关方式格式化和解析日期或时间的DateFormat方法就只在Date类里有。

DateFormat方法也有它自己的问题。比如,它不是线程安全的。这意味着两个线程如果尝试使用同一个formatter解析日期,你可能会得到无法预期的结果。

最后, Date和Calendar类都是可以变的。能把2014年3月18日修改成4月18日意味着什么 呢?这种设计会将你拖入维护的噩梦,接下来的一章,我们会讨论函数式编程,你在该章中会了 解到更多的细节。

所有这些缺陷和不一致导致用户们转投第三方的日期和时间库,比如Joda-Time。为了解决 这些问题,Oracle决定在原生的Java API中提供高质量的日期和时间支持。所以,你会看到Java 8 在java.time包中整合了很多Joda-Time的特性。

这一章中,我们会一起探索新的日期和时间API所提供的新特性。我们从最基本的用例入手, 比如创建同时适合人与机器的日期和时间,逐渐转入到日期和时间API更高级的一些应用,比如 操纵、解析、打印输出日期时间对象,使用不同的时区和年历。

LocalDate、LocalTime、Instant、Duration 以及 Period

让我们从探索如何创建简单的日期和时间间隔入手。java.time包中提供了很多新的类可以 帮你解决问题,它们是LocalDate、LocalTime、Instant、Duration和Period。

使用 LocalDate 和 LocalTime

LocalDate

1
2
public final class LocalDate
        implements Temporal, TemporalAdjuster, ChronoLocalDate, Serializable {

开始使用新的日期和时间API时,你最先碰到的可能是LocalDate类。开始使用新的日期和时间API时,你最先碰到的可能是LocalDate类。该类的实例是一个不 可变对象,它只提供了简单的日期,并不含当天的时间信息。另外,它也不附带任何与时区相关 的信息。

你可以通过静态工厂方法of创建一个LocalDate实例。LocalDate实例提供了多种方法来 读取常用的值,比如年份、月份、星期几等,如下所示。

代码清单12-1 创建一个LocalDate对象并读取其值

1
2
3
4
5
6
7
8
        LocalDate date = LocalDate.of(2014, 3, 19);
        int year = date.getYear();
        Month month = date.getMonth(); // 12个枚举值
        int monthVal = date.getMonthValue(); // 1-12
        int day = date.getDayOfMonth();
        DayOfWeek dow = date.getDayOfWeek(); // TUESDAY
        int len = date.lengthOfMonth(); // 31(days in March)
        boolean leap = date.isLeapYear(); // 是否是闰年

你还可以使用工厂方法从系统时钟中获取当前的日期:

1
LocalDate today = LocalDate.now();

本章剩余的部分会探讨所有日期时间类,这些类都提供了类似的工厂方法。你还可以通过传递一个TemporalField参数给get方法拿到同样的信息。TemporalField是一个接口,它定 义了如何访问temporal对象某个字段的值。ChronoField枚举实现了这一接口,所以你可以很 方便地使用get方法得到枚举元素的值,如下所示。

代码清单12-2 使用TemporalField读取LocalDate的值

1
2
3
int year = date.get(ChronoField.YEAR);
int month = date.get(ChronoField.MONTH_OF_YEAR);
int day = date.get(ChronoField.DAY_OF_MONTH);

LocalTime

1
2
public final class LocalTime
        implements Temporal, TemporalAdjuster, Comparable<LocalTime>, Serializable {

类似地,一天中的时间,比如13:45:20,可以使用LocalTime类表示。你可以使用of重载的 两个工厂方法创建LocalTime的实例。第一个重载函数接收小时和分钟,第二个重载函数同时还 接收秒。

同LocalDate一样,LocalTime类也提供了一些getter方法访问这些变量的值,如下 所示。

代码清单12-3 创建LocalTime并读取其值

1
2
3
4
LocalTime time = LocalTime.of(13, 45, 20);
int hour = time.getHour();
int minute = time.getMinute();
int second = time.getSecond();

LocalDate和LocalTime都可以通过解析代表它们的字符串创建。使用静态方法parse,你可以实现这一目的:

1
2
        LocalDate date = LocalDate.parse("2014-03-18");
        LocalTime time = LocalTime.parse("13:45:20");

你可以向parse方法传递一个DateTimeFormatter。该类的实例定义了如何格式化一个日 期或者时间对象。正如我们之前所介绍的,它是替换老版java.util.DateFormat的推荐替代 品。我们会在12.2节展开介绍怎样使用DateTimeFormatter。同时,也请注意,一旦传递的字 符串参数无法被解析为合法的LocalDate或LocalTime对象,这两个parse方法都会抛出一个继 承自RuntimeException的DateTimeParseException异常。

合并日期和时间

LocalDateTime

1
2
public final class LocalDateTime
        implements Temporal, TemporalAdjuster, ChronoLocalDateTime<LocalDate>, Serializable {

这个复合类名叫LocalDateTime,是LocalDate和LocalTime的合体。它同时表示了日期 和时间,但不带有时区信息,你可以直接创建,也可以通过合并日期和时间对象构造,如下所示。

代码清单12-4 直接创建LocalDateTime对象,或者通过合并日期和时间的方式创建

1
2
3
4
5
6
// 2014-03-18T13:45:20
LocalDateTime dt1 = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45, 20);
LocalDateTime dt2 = LocalDateTime.of(date, time);
LocalDateTime dt3 = date.atTime(13, 45, 20);
LocalDateTime dt4 = date.atTime(time);
LocalDateTime dt5 = time.atDate(date);

注意,通过它们各自的atTime或者atDate方法,向LocalDate传递一个时间对象,或者向LocalTime 传递一个日期对象的方式, 你可以创建一个 LocalDateTime 对象。 你也可以使用 toLocalDate 或者 toLocalTime 方法,从 LocalDateTime 中提取LocalDate 或者 LocalTime 组件:

1
2
LocalDate date1 = dt1.toLocalDate(); 
LocalTime time1 = dt1.toLocalTime();

机器的日期和时间格式

Instant

1
2
public final class Instant
        implements Temporal, TemporalAdjuster, Comparable<Instant>, Serializable {

作为人,我们习惯于以星期几、几号、几点、几分这样的方式理解日期和时间。毫无疑问, 这种方式对于计算机而言并不容易理解。从计算机的角度来看,建模时间最自然的格式是表示一 个持续时间段上某个点的单一大整型数。这也是新的 java.time.Instant 类对时间建模的方 式,基本上它是以Unix元年时间(传统的设定为UTC时区1970年1月1日午夜时分)开始所经历的 秒数进行计算。

你可以通过向静态工厂方法ofEpochSecond传递一个代表秒数的值创建一个该类的实例。静 态工厂方法ofEpochSecond还有一个增强的重载版本,它接收第二个以纳秒为单位的参数值,对 传入作为秒数的参数进行调整。重载的版本会调整纳秒参数,确保保存的纳秒分片在0到999 999 999之间。这意味着下面这些对ofEpochSecond工厂方法的调用会返回同样的Instant对象:

1
2
3
4
Instant instant = Instant.ofEpochSecond(3);
Instant instant2 = Instant.ofEpochSecond(3, 0);
Instant instant3 = Instant.ofEpochSecond(2, 1_000_000_000);
Instant instant4 = Instant.ofEpochSecond(4, -1_000_000_000);

正如你已经在 LocalDate 及其他为便于阅读而设计的日期时间类中所看到的那样, Instant类也支持静态工厂方法now,它能够帮你获取当前时刻的时间戳。我们想要特别强调一 点,Instant的设计初衷是为了便于机器使用。它包含的是由秒及纳秒所构成的数字。所以,它 无法处理那些我们非常容易理解的时间单位。比如下面这段语句:

1
int day = Instant.now().get(ChronoField.DAY_OF_MONTH);

它会抛出下面这样的异常:

1
java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: DayOfMonth

但是你可以通过Duration和Period类使用Instant,接下来我们会对这部分内容进行介绍。

定义Duration或Period

目前为止,你看到的所有类都实现了Temporal接口,Temporal接口定义了如何读取和操纵为时间建模的对象的值。之前的介绍中,我们已经了解了创建Temporal实例的几种方法。很自 然地你会想到,我们需要创建两个Temporal对象之间的duration。Duration类的静态工厂方法between就是为这个目的而设计的。你可以创建两个LocalTimes对象、两个LocalDateTimes对象,或者两个Instant对象之间的duration,如下所示:

1
2
3
Duration d1 = Duration.between(time1, time2);
Duration d2 = Duration.between(dateTime1, dateTime2); 
Duration d3 = Duration.between(instant1, instant2);

由于LocalDateTime和Instant是为不同的目的而设计的,一个是为了便于人阅读使用, 另一个是为了便于机器处理, 所以你不能将二者混用。 如果你试图在这两类对象之间创建 duration,会触发一个DateTimeException异常。此外,由于Duration类主要用于以秒和纳秒衡量时间的长短,你不能向between方法传递一个LocalDate对象做参数(虽然编译不报错但是运行时会报UnsupportedTemporalTypeException)。

如果你需要以年、月或者日的方式对多个时间单位建模,可以使用Period类。使用该类的 工厂方法between,你可以使用得到两个LocalDate之间的时长,如下所示:

1
2
3
4
        Period tenDays = Period.between(
                LocalDate.of(2014, 3, 8),
                LocalDate.of(2014, 3, 18)
        );

最后,Duration和Period类都提供了很多非常方便的工厂类,直接创建对应的实例;换 句话说,就像下面这段代码那样,不再是只能以两个temporal对象的差值的方式来定义它们的 对象。

代码清单12-5 创建Duration和Period对象

1
2
3
4
5
6
Duration threeMinutes = Duration.ofMinutes(3); 
Duration threeMinutes = Duration.of(3, ChronoUnit.MINUTES);

Period tenDays = Period.ofDays(10); 
Period threeWeeks = Period.ofWeeks(3); 
Period twoYearsSixMonthsOneDay = Period.of(2, 6, 1);

Duration类和Period类共享了很多相似的方法,参见表12-1所示。

https://gitee.com/lienhui68/picStore/raw/master/null/20200814195218.png

截至目前,我们介绍的这些日期时间对象都是不可修改的,这是为了更好地支持函数式编程,确保线程安全,保持领域模式一致性而做出的重大设计决定。当然,新的日期和时间API也 提供了一些便利的方法来创建这些对象的可变版本。比如,你可能希望在已有的LocalDate实例 上增加3天。我们在下一节中会针对这一主题进行介绍。除此之外,我们还会介绍如何依据指定 的模式,比如dd/MM/yyyy,创建日期时间格式器,以及如何使用这种格式器解析和输出日期。

操纵、解析和格式化日期

如果你已经有一个LocalDate对象,想要创建它的一个修改版,最直接也最简单的方法是使 用withAttribute方法。withAttribute方法会创建对象的一个副本,并按照需要修改它的属性。注意,下面的这段代码中所有的方法都返回一个修改了属性的对象。它们都不会修改原来的 对象!

代码清单12-6 以比较直观的方式操纵LocalDate的属性

1
2
3
4
LocalDate date1 = LocalDate.of(2014, 3, 18); 
LocalDate date2 = date1.withYear(2011); 
LocalDate date3 = date2.withDayOfMonth(25); 
LocalDate date4 = date3.with(ChronoField.MONTH_OF_YEAR, 9);

采用更通用的with方法能达到同样的目的,它接受的第一个参数是一个TemporalField对 象,格式类似代码清单12-6的最后一行。最后这一行中使用的with方法和代码清单12-2中的get 方法有些类似。它们都声明于Temporal接口,所有的日期和时间API类都实现这两个方法,它 们定义了单点的时间,比如LocalDate、LocalTime、LocalDateTime以及Instant。更确切 地说,使用get和with方法,我们可以将Temporal对象值的读取和修改区分开。如果Temporal 对象不支持请求访问的字段,它会抛出一个UnsupportedTemporalTypeException异常,比 如试图访问 Instant 对象的 ChronoField.MONTH_OF_YEAR 字段, 或者 LocalDate 对象的 ChronoField.NANO_OF_SECOND字段时都会抛出这样的异常。

它甚至能以声明的方式操纵LocalDate对象。比如,你可以像下面这段代码那样加上或者减 去一段时间。

代码清单12-7 以相对方式修改LocalDate对象的属性

1
2
3
4
LocalDate date1 = LocalDate.now();
LocalDate date2 = date1.plusWeeks(1);
LocalDate date3 = date2.minusYears(3);
LocalDate date4 = date3.plus(6, ChronoUnit.MONTHS);

与我们刚才介绍的get和with方法类似,代码清单12-7中最后一行使用的plus方法也是通用 方法,它和minus方法都声明于Temporal接口中。通过这些方法,对TemporalUnit对象加上 或者减去一个数字, 我们能非常方便地将 Temporal 对象前溯或者回滚至某个时间段, 通过 ChronoUnit枚举我们可以非常方便地实现TemporalUnit接口。

大概你已经猜到,像LocalDate、LocalTime、LocalDateTime以及Instant这样表示时 间点的日期时间类提供了大量通用的方法,表12-2对这些通用的方法进行了总结。

https://gitee.com/lienhui68/picStore/raw/master/null/20200814213625.png

使用TemporalAdjuster

截至目前,你所看到的所有日期操作都是相对比较直接的。有的时候,你需要进行一些更加 复杂的操作,比如,将日期调整到下个周日、下个工作日,或者是本月的最后一天。这时,你可 以使用重载版本的with方法,向其传递一个提供了更多定制化选择的TemporalAdjuster对象, 更加灵活地处理日期。 对于最常见的用例, 日期和时间API已经提供了大量预定义的 TemporalAdjuster。你可以通过TemporalAdjuster类的静态工厂方法访问它们,如下所示。

代码清单12-8 使用预定义的TemporalAdjuster

1
2
3
LocalDate date1 = LocalDate.now();
LocalDate date2 = date1.with(nextOrSame(DayOfWeek.SUNDAY));
LocalDate date3 = date2.with(lastDayOfMonth());

表12-3提供了TemporalAdjuster中包含的工厂方法列表。

https://gitee.com/lienhui68/picStore/raw/master/null/20200814213901.png

dayOfWeekInMonth解释有误

1
2
3
4
5
 * @param ordinal  the week within the month, unbounded but typically from -5 to 5
     * @param dayOfWeek  the day-of-week, not null
     * @return the day-of-week in month adjuster, not null
     */
    public static TemporalAdjuster dayOfWeekInMonth(int ordinal, DayOfWeek dayOfWeek) {
1
LocalDate date4 = date1.with(dayOfWeekInMonth(1, DayOfWeek.MONDAY));

会返回本月第一周的星期一所在日期,也就是2020-08-03

正如我们看到的,使用TemporalAdjuster我们可以进行更加复杂的日期操作,而且这些方 法的名称也非常直观,方法名基本就是问题陈述。此外,即使你没有找到符合你要求的预定义的 TemporalAdjuster , 创建你自己的 TemporalAdjuster 也并非难事。 实际上, Temporal- Adjuster接口只声明了单一的一个方法(这使得它成为了一个函数式接口),定义如下。

1
2
3
4
@FunctionalInterface
public interface TemporalAdjuster {
    Temporal adjustInto(Temporal temporal);
}

这意味着 TemporalAdjuster 接口的实现需要定义如何将一个 Temporal 对象转换为另一 个Temporal对象。你可以把它看成一个UnaryOperator<Temporal>。花几分钟时间完成下面的测验,练习一下我们到目前为止所学习的东西,请实现你自己的TemporalAdjuster。

实现一个定制的TemporalAdjuster

请设计一个NextWorkingDay类,该类实现了TemporalAdjuster接口,能够计算明天

的日期,同时过滤掉周六和周日这些节假日。格式如下所示:

1
date = date.with(new NextWorkingDay());

如果当天的星期介于周一至周五之间,日期向后移动一天;如果当天是周六或者周日,则返回下一个周一。

下面是参考的NextWorkingDay类的实现。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class NextWorkingDay implements TemporalAdjuster {

    @Override

    public Temporal adjustInto(Temporal temporal) {
        DayOfWeek dow = DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));

        // 正常情况,增加1天
        int dayToAdd = 1;
        // 如果当天是周五,增加3天
        if (dow == DayOfWeek.FRIDAY) dayToAdd = 3;
        // 如果当天是周六,增加2天
        else if (dow == DayOfWeek.SATURDAY) dayToAdd = 2;

        return temporal.plus(dayToAdd, ChronoUnit.DAYS);
    }
}

该TemporalAdjuster通常情况下将日期往后顺延一天,如果当天是周六或者周日,则依据情况分别将日期顺延3天或者2天。注意,由于TemporalAdjuster是一个函数式接口,你也可以以Lambda表达式的方式向该adjuster接口传递行为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
LocalDate date4 = date1.with(temporal -> {
            DayOfWeek dow = DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));

            // 正常情况,增加1天
            int dayToAdd = 1;
            // 如果当天是周五,增加3天
            if (dow == DayOfWeek.FRIDAY) dayToAdd = 3;
                // 如果当天是周六,增加2天
            else if (dow == DayOfWeek.SATURDAY) dayToAdd = 2;

            return temporal.plus(dayToAdd, ChronoUnit.DAYS);
        });

你大概会希望在你代码的多个地方使用同样的方式去操作日期,为了达到这一目的,我们建议你像我们的示例那样将它的逻辑封装到一个类中。对于你经常使用的操作,都应该采用类似的方式,进行封装。最终,你会创建自己的类库,让你和你的团队能轻松地实现代码复用。

如果你想要使用Lambda表达式定义 TemporalAdjuster 对象, 推荐使用 Temporal-Adjusters类的静态工厂方法ofDateAdjuster,它接受一个UnaryOperator<LocalDate>类型的参数,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
TemporalAdjuster nextWorkingDay = TemporalAdjusters.ofDateAdjuster(temporal -> {
            DayOfWeek dow = DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));

            // 正常情况,增加1天
            int dayToAdd = 1;
            // 如果当天是周五,增加3天
            if (dow == DayOfWeek.FRIDAY) dayToAdd = 3;
                // 如果当天是周六,增加2天
            else if (dow == DayOfWeek.SATURDAY) dayToAdd = 2;

            return temporal.plus(dayToAdd, ChronoUnit.DAYS);
        });

你可能希望对你的日期时间对象进行的另外一个通用操作是,依据你的业务领域以不同的格 式打印输出这些日期和时间对象。类似地,你可能也需要将那些格式的字符串转换为实际的日期 对象。接下来的一节,我们会演示新的日期和时间API提供那些机制是如何完成这些任务的。

打印输出及解析日期——时间对象

format

处理日期和时间对象时,格式化以及解析日期时间对象是另一个非常重要的功能。新的 java.time.format 包就是特别为这个目的而设计的。 这个包中, 最重要的类是 DateTimeFormatter。创建格式器最简单的方法是通过它的静态工厂方法以及常量。像BASIC_ISO_DATE 和 ISO_LOCAL_DATE 这 样 的 常 量 是 DateTimeFormatter 类 的 预 定 义 实 例 。 所 有 的 DateTimeFormatter实例都能用于以一定的格式创建代表特定日期或时间的字符串。比如,下 面的这个例子中,我们使用了两个不同的格式器生成了字符串:

1
2
3
LocalDate date = LocalDate.now();
String s1 = date.format(DateTimeFormatter.BASIC_ISO_DATE);
String s2 = date.format(DateTimeFormatter.ISO_LOCAL_DATE);

parse

你也可以通过解析代表日期或时间的字符串重新创建该日期对象。所有的日期和时间API 都提供了表示时间点或者时间段的工厂方法,你可以使用工厂方法parse达到重创该日期对象 的目的:

1
2
LocalDate date1 = LocalDate.parse("20140318", DateTimeFormatter.BASIC_ISO_DATE);
LocalDate date2 = LocalDate.parse("2014-03-18", DateTimeFormatter.ISO_LOCAL_DATE);

和老的 java.util.DateFormat 相比较,所有的 DateTimeFormatter 实例都是线程安全 的。所以,你能够以单例模式创建格式器实例,就像DateTimeFormatter所定义的那些常量, 并能在多个线程间共享这些实例。DateTimeFormatter类还支持一个静态工厂方法,它可以按 照某个特定的模式创建格式器,代码清单如下。

代码清单12-10 按照某个模式创建DateTimeFormatter

1
2
3
4
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
LocalDate date1 = LocalDate.now();
String formattedDate = date1.format(formatter);
LocalDate date2 = LocalDate.parse(formattedDate, formatter);

这段代码中,LocalDate的formate方法使用指定的模式生成了一个代表该日期的字符串。 紧接着,静态的parse方法使用同样的格式器解析了刚才生成的字符串,并重建了该日期对象。 ofPattern方法也提供了一个重载的版本,使用它你可以创建某个Locale的格式器

代码清单如下所示。

代码清单12-11 创建一个本地化的DateTimeFormatter

1
2
3
4
5
        DateTimeFormatter italianFormatter = DateTimeFormatter.ofPattern("d. MMMM yyyy", Locale.ITALIAN);
        DateTimeFormatter chineseFormatter = DateTimeFormatter.ofPattern("d. MMMM yyyy", Locale.CHINA);
        LocalDate date = LocalDate.now();
        String formattedDate1 = date.format(italianFormatter); // 14. agosto 2020
        String formattedDate2 = date.format(chineseFormatter); // 14. 八月 2020

最后,如果你还需要更加细粒度的控制,DateTimeFormatterBuilder类还提供了更复杂 的格式器,你可以选择恰当的方法,一步一步地构造自己的格式器。另外,它还提供了非常强大 的解析功能,比如区分大小写的解析、柔性解析(允许解析器使用启发式的机制去解析输入,不 精 确 地 匹 配 指 定 的 模 式 )、 填 充 , 以 及 在 格 式 器 中 指 定 可 选 节 。 比 如 , 你 可 以 通 过 DateTimeFormatterBuilder 自己编程实现我们在代码清单12-11中使用的 italianFor- matter,代码清单如下。

代码清单12-12 构造一个DateTimeFormatter

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
DateTimeFormatter italianFormatter = new DateTimeFormatterBuilder()
        .appendText(ChronoField.DAY_OF_MONTH)
        .appendLiteral("# ")
        .appendText(ChronoField.MONTH_OF_YEAR)
        .appendLiteral(" ").appendText(ChronoField.YEAR)
        .parseCaseInsensitive()
        .toFormatter(Locale.ITALIAN);

DateTimeFormatter chineseFormatter = DateTimeFormatter.ofPattern("d. MMMM yyyy", Locale.CHINA);
LocalDate date = LocalDate.now();
String formattedDate1 = date.format(italianFormatter); // 14# agosto 2020
String formattedDate2 = date.format(chineseFormatter); // 14. 八月 2020

目前为止,你已经学习了如何创建、操纵、格式化以及解析时间点和时间段,但是你还不了 解如何处理日期和时间之间的微妙关系。比如,你可能需要处理不同的时区,或者由于不同的历 法系统带来的差异。接下来的一节,我们会探究如何使用新的日期和时间API解决这些问题。

处理不同的时区和历法

之前你看到的日期和时间的种类都不包含时区信息。时区的处理是新版日期和时间API新增 加的重要功能,使用新版日期和时间API时区的处理被极大地简化了。新的java.time.ZoneId 类是老版java.util.TimeZone的替代品。它的设计目标就是要让你无需为时区处理的复杂和 繁琐而操心,比如处理夏令时(Daylight Saving Time,DST)这种问题。跟其他日期和时间类一 样,ZoneId类也是无法修改的。

时区是按照一定的规则将区域划分成的标准时间相同的区间。在ZoneRules这个类中包含了 40个这样的实例。你可以简单地通过调用ZoneId的getRules()得到指定时区的规则。每个特定 的ZoneId对象都由一个地区ID标识,比如:

1
ZoneId romeZone = ZoneId.of("Europe/Rome");

地区ID都为“{区域}/{城市}”的格式,这些地区集合的设定都由英特网编号分配机构(IANA) 的时区数据库提供。你可以通过Java 8的新方法toZoneId将一个老的时区对象转换为ZoneId:

1
ZoneId zoneId = TimeZone.getDefault().toZoneId();

一旦得到一个ZoneId对象,你就可以将它与LocalDate、LocalDateTime或者是Instant 对象整合起来,构造为一个ZonedDateTime实例,它代表了相对于指定时区的时间点,代码清单如下所示。

代码清单12-13 为时间点添加时区信息

1
2
3
4
5
6
7
8
9
ZoneId romeZone = ZoneId.of("Europe/Rome");
LocalDate date = LocalDate.now();
ZonedDateTime zdt1 = date.atStartOfDay(romeZone);

LocalDateTime dateTime = LocalDateTime.now();
ZonedDateTime zdt2 = dateTime.atZone(romeZone);

Instant instant = Instant.now();
ZonedDateTime zdt3 = instant.atZone(romeZone);

图12-1对 ZonedDateTime 的组成部分进行了说明, 相信能够帮助你理解 LocaleDate 、 LocalTime、LocalDateTime以及ZoneId之间的差异。

https://gitee.com/lienhui68/picStore/raw/master/null/20200814225906.png

通过ZoneId,你还可以将LocalDateTime转换为Instant:

1
2
LocalDateTime dateTime = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45); 
Instant instantFromDateTime = dateTime.toInstant(romeZone);

你也可以通过反向的方式得到LocalDateTime对象:

1
2
Instant instant = Instant.now(); 
LocalDateTime timeFromInstant = LocalDateTime.ofInstant(instant, romeZone);

利用和 UTC/格林尼治时间的固定偏差计算时区

另一种比较通用的表达时区的方式是利用当前时区和UTC/格林尼治的固定偏差。比如,基于这个理论,你可以说“纽约落后于伦敦5小时”。这种情况下,你可以使用ZoneOffset类,它 是ZoneId的一个子类,表示的是当前时间和伦敦格林尼治子午线时间的差异:

1
ZoneOffset newYorkOffset = ZoneOffset.of("-05:00");

“05:00”的偏差实际上对应的是美国东部标准时间。注意,使用这种方式定义的ZoneOffset 并未考虑任何日光时的影响,所以在大多数情况下,不推荐使用。由于ZoneOffset也是ZoneId, 所以你可以像代码清单12-13那样使用它。你甚至还可以创建这样的OffsetDateTime,它使用 ISO-8601的历法系统,以相对于UTC/格林尼治时间的偏差方式表示日期时间。

1
2
LocalDateTime dateTime = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45); 
OffsetDateTime dateTimeInNewYork = OffsetDateTime.of(date, newYorkOffset);

新版的日期和时间API还提供了另一个高级特性,即对非ISO历法系统(non-ISO calendaring) 的支持。

使用别的日历系统

ISO-8601日历系统是世界文明日历系统的事实标准。但是,Java 8中另外还提供了4种其他的 日历系统。 这些日历系统中的每一个都有一个对应的日志类, 分别是 ThaiBuddhistDate 、 MinguoDate 、 JapaneseDate 以 及 HijrahDate 。 所 有 这 些 类 以 及 LocalDate 都 实 现 了 ChronoLocalDate接口,能够对公历的日期进行建模。利用LocalDate对象,你可以创建这些 类的实例。更通用地说,使用它们提供的静态工厂方法,你可以创建任何一个Temporal对象的 实例,如下所示:

1
2
LocalDate date = LocalDate.of(2014, Month.MARCH, 18); 
JapaneseDate japaneseDate = JapaneseDate.from(date);

或者,你还可以为某个Locale显式地创建日历系统,接着创建该Locale对应的日期的实例。 新的日期和时间API中, Chronology 接口建模了一个日历系统, 使用它的静态工厂方法 ofLocale,可以得到它的一个实例,代码如下:

1
2
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日历中计算当前伊斯兰年中斋月的起始和终止 日期:

https://gitee.com/lienhui68/picStore/raw/master/null/20200814230337.png

小结

这一章中,你应该掌握下面这些内容。

  • Java 8之前老版的 java.util.Date类以及其他用于建模日期时间的类有很多不一致及设计上的缺陷,包括易变性以及糟糕的偏移值、默认值和命名。
  • 新版的日期和时间API中,日期-时间对象是不可变的。
  • 新的API提供了两种不同的时间表示方式,有效地区分了运行时人和机器的不同需求。
  • 你可以用绝对或者相对的方式操纵日期和时间,操作的结果总是返回一个新的实例,老的日期时间对象不会发生变化
  • TemporalAdjuster让你能够用更精细的方式操纵日期,不再局限于一次只能改变它的一个值,并且你还可按照需求定义自己的日期转换器。
  • 你现在可以按照特定的格式需求,定义自己的格式器,打印输出或者解析日期时间对象。这些格式器可以通过模板创建,也可以自己编程创建,并且它们都是线程安全的。
  • 你可以用相对于某个地区/位置的方式,或者以与UTC/格林尼治时间的绝对偏差的方式表示时区,并将其应用到日期-时间对象上,对其进行本地化。
  • 你现在可以使用不同于ISO-8601标准系统的其他日历系统了。