12.1 LocalDateLocalTimeLocalDateTimeInstantDuration以及Period

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

12.1.1 使用LocalDateLocalTime

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

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

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

  1. LocalDate date = LocalDate.of(2017, 9, 21); ←---- 2017-09-21
  2. int year = date.getYear(); ←---- 2017
  3. Month month = date.getMonth(); ←---- SEPTEMBER
  4. int day = date.getDayOfMonth(); ←---- 21
  5. DayOfWeek dow = date.getDayOfWeek(); ←---- THURSDAY
  6. int len = date.lengthOfMonth(); ←---- 30 (days in September)
  7. boolean leap = date.isLeapYear(); ←---- false (not a leap year)

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

  1. LocalDate today = LocalDate.now();

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

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

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

你可以使用Java内建的getYear()getMonthValue()getDayOfMonth()方法,以更具可读性的方式访问这些信息,如下所示:

  1. int year = date.getYear();
  2. int month = date.getMonthValue();
  3. int day = date.getDayOfMonth();

类似地,一天中的时间,比如13:45:20,可以使用LocalTime类表示。你可以使用of重载的两个工厂方法创建LocalTime的实例。第一个重载函数接受小时和分钟,第二个重载函数同时还接受秒。同LocalDate类一样,LocalTime类也提供了一些getter方法访问这些变量的值,如下所示。

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

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

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

  1. LocalDate date = LocalDate.parse("2017-09-21");
  2. LocalTime time = LocalTime.parse("13:45:20");

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

12.1.2 合并日期和时间

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

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

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

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

  1. LocalDate date1 = dt1.toLocalDate(); ←---- 2017-09-21
  2. LocalTime time1 = dt1.toLocalTime(); ←---- 13:45:20

12.1.3 机器的日期和时间格式

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

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

  1. Instant.ofEpochSecond(3);
  2. Instant.ofEpochSecond(3, 0);
  3. Instant.ofEpochSecond(2, 1_000_000_000); ←---- 2秒之后再加上10亿纳秒(1 秒)
  4. Instant.ofEpochSecond(4, -1_000_000_000); ←---- 4秒之前的10亿纳秒(1 秒)

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

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

会抛出下面这样的异常:

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

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

12.1.4 定义DurationPeriod

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

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

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

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

  1. Period tenDays = Period.between(LocalDate.of(2017, 9, 11),
  2. LocalDate.of(2017, 9, 21));

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

代码清单 12-5 创建DurationPeriod对象

  1. Duration threeMinutes = Duration.ofMinutes(3);
  2. Duration threeMinutes = Duration.of(3, ChronoUnit.MINUTES);
  3. Period tenDays = Period.ofDays(10);
  4. Period threeWeeks = Period.ofWeeks(3);
  5. Period twoYearsSixMonthsOneDay = Period.of(2, 6, 1);

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

表 12-1 日期–时间类中表示时间间隔的通用方法

方法名 是否是静态方法 方法描述
between 创建两个时间点之间的interval
from 由一个临时时间点创建interval
of 由它的组成部分创建interval的实例
parse 由字符串创建interval的实例
addTo 创建该interval的副本,并将其叠加到某个指定的temporal对象
get 读取该interval的状态
isNegative 检查该interval是否为负值,不包含零
isZero 检查该interval的时长是否为零
minus 通过减去一定的时间创建该interval的副本
multipliedBy interval的值乘以某个标量创建该interval的副本
negated 以忽略某个时长的方式创建该interval的副本
plus 以增加某个指定的时长的方式创建该interval的副本
subtractFrom 从指定的temporal对象中减去该interval

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