新的日期时间API

本文参考书籍《Java 8实战》,陆明刚、劳佳  译,如有侵权,请联系删除!


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

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

它的打印输出效果为:

    Tue Mar 18 00:00:00 CET 2014

看起来不那么直观。此外,Date类的toString方法返回的字符串也容易误导人。以我们的例子而言,它的返回值中甚至还包含了JVM的默认时区CET,即中欧时间(CentralEurope 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的特性。让我们从探索如何创建简单的日期和时间间隔入手。java.time包中提供了很多新的类可以帮你解决问题,它们是LocalDate、 LocalTime、 Instant、 Duration和Period。

LocalDate 和 LocalTime

LocalDate类的实例是一个不可变对象,它只提供了简单的日期,并不含当天的时间信息。另外,它也不附带任何与时区相关的信息。可以通过静态工厂方法of创建一个LocalDate实例。 LocalDate实例提供了多种方法来读取常用的值,比如年份、月份、星期几等,如下所示:

        LocalDate date = LocalDate.of(2014, 3, 18);
        int year = date.getYear(); // 2014
        Month month = date.getMonth(); // MARCH
        int day = date.getDayOfMonth(); // 18
        DayOfWeek dow = date.getDayOfWeek(); // TUESDAY
        int len = date.lengthOfMonth(); // 31 (days in March)
        boolean leap = date.isLeapYear(); // false (not a leap year,不是闰年)

还可以通过传递一个TemporalField参数给get方法拿到同样的信息。 TemporalField是一个接口,它定义了如何访问temporal对象个字段的值。 ChronoField枚举实现了这一接口,所以你可以很方便地使用get方法得到枚举元素的值,如下所示:

        int y = date.get(ChronoField.YEAR);
        int m = date.get(ChronoField.MONTH_OF_YEAR);
        int d = date.get(ChronoField.DAY_OF_MONTH);

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

    LocalDate today = LocalDate.now();

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

        LocalTime time = LocalTime.of(13, 45, 20); // 13:45:20
        int hour = time.getHour(); // 13
        int minute = time.getMinute(); // 45
        int second = time.getSecond(); // 20

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

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

可以向parse方法传递一个DateTimeFormatter(第二个参数),该类的实例定义了如何格式化一个日期或者时间对象,它是替换老版java.util.DateFormat的推荐替代品。后面会详细介绍DateTimeFormatter。一旦传递的字符串参数无法被解析为合法的LocalDate或LocalTime对象,这两个parse方法都会抛出一个继承自RuntimeException的DateTimeParseException异常。

LocalDateTime

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

        LocalDateTime dt1 = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45, 20); // 2014-03-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);

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

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

Instant

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

可以通过向Instant类的静态工厂方法ofEpochSecond传递一个代表秒数的值创建一个该类的实例,例如:

    Instant instant = Instant.ofEpochSecond(44 * 365 * 86400);

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

    Instant.ofEpochSecond(3); 
    Instant.ofEpochSecond(3, 0);
    Instant.ofEpochSecond(2, 1000000000); // 2秒之后再加上100万纳秒(1秒)
    Instant.ofEpochSecond(4, -1000000000); // 4秒之前的100万纳秒(1秒)

Instant类也支持静态工厂方法now,它能够获取当前时刻的时间戳:

    Instant now = Instant.now();

特别强调一点, Instant的设计初衷是为了便于机器使用。它包含的是由秒及纳秒所构成的数字。所以,它无法处理那些我们非常容易理解的时间单位。比如下面这段语句:

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

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

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

Duration 和 Period

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

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

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

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

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

    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);

操纵、解析和格式化日期

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

    LocalDate date1 = LocalDate.of(2014, 3, 18); // 2014-03-18
    LocalDate date2 = date1.withYear(2011); // 2011-03-18
    LocalDate date3 = date2.withDayOfMonth(25); // 2011-03-25
    LocalDate date4 = date3.with(ChronoField.MONTH_OF_YEAR, 9); // 2011-09-25

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

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

    LocalDate date1 = LocalDate.of(2014, 3, 18); // 2014-03-18
    LocalDate date2 = date1.plusWeeks(1); // 2014-03-25
    LocalDate date3 = date2.minusYears(3); // 2011-03-25
    LocalDate date4 = date3.plus(6, ChronoUnit.MONTHS); // 2011-09-25

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

下表对这些通用的方法进行了总结。

方法名 是否是静态方法 描述
from 依据传入的 Temporal 对象创建对象实例
now 依据系统时钟创建 Temporal 对象
of 由 Temporal 对象的某个部分创建该对象的实例
parse 由字符串创建 Temporal 对象的实例
atOffset 将 Temporal 对象和某个时区偏移相结合
atZone 将 Temporal 对象和某个时区相结合
format 使用某个指定的格式器将Temporal 对象转换为字符串(Instant 类不提供该方法)
get 读取 Temporal 对象的某一部分的值
minus 创建 Temporal 对象的一个副本,通过将当前 Temporal 对象的值减去一定的时长
创建该副本
plus 创建 Temporal 对象的一个副本,通过将当前 Temporal 对象的值加上一定的时长
创建该副本
with 以该 Temporal 对象为模板,对某些状态进行修改创建该对象的副本

TemporalAdjuster

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

    LocalDate date1 = LocalDate.of(2014, 3, 18); // 2014-03-18
    LocalDate date2 = date1.with(nextOrSame(DayOfWeek.SUNDAY)); // 2014-03-23,注意静态导入
    LocalDate date3 = date2.with(lastDayOfMonth()); // 2014-03-31

下表提供了TemporalAdjusters中包含的工厂方法列表:

方法名 描述
dayOfWeekInMonth 创建一个新的日期,它的值为同一个月中每一周的第几天
firstDayOfMonth 创建一个新的日期,它的值为当月的第一天
firstDayOfNextMonth 创建一个新的日期,它的值为下月的第一天
firstDayOfNextYear 创建一个新的日期,它的值为明年的第一天
firstDayOfYear 创建一个新的日期,它的值为当年的第一天
firstInMonth 创建一个新的日期,它的值为同一个月中,第一个符合星期几要求的值
lastDayOfMonth 创建一个新的日期,它的值为当月的最后一天
lastDayOfNextMonth 创建一个新的日期,它的值为下月的最后一天
lastDayOfNextYear 创建一个新的日期,它的值为明年的最后一天
lastDayOfYear 创建一个新的日期,它的值为今年的最后一天
lastInMonth 创建一个新的日期,它的值为同一个月中,最后一个符合星期几要求的值
next/previous 创建一个新的日期,并将其值设定为日期调整后或者调整前,第一个符合指定星期几要求的日期
nextOrSame/previousOrSame 创建一个新的日期,并将其值设定为日期调整后或者调整前,第一个符合指定星期几要求的日期,如果该日期已经符合要求,直接返回该对象

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

@FunctionalInterface
public interface TemporalAdjuster {

    Temporal adjustInto(Temporal temporal);

}

这意味着TemporalAdjuster接口的实现需要定义如何将一个Temporal对象转换为另一个Temporal对象,可以把它看成一个UnaryOperator,具体如何实现可以参考TemporalAdjusters源码。

DateTimeFormatter

处理日期和时间对象时,格式化以及解析日期时间对象是另一个非常重要的功能。java.time.format.DateTimeFormatter类就是为这个目的而设计的。DateTimeFormatter是一个可以替代DateFormat的日期时间格式化器。可以通过它的静态工厂方法或预定义常量得到DateTimeFormatter的实例。下面的这个例子中,我们使用了两个不同的格式器生成了字符串:

    LocalDate date = LocalDate.of(2014, 3, 18);
    String s1 = date.format(DateTimeFormatter.BASIC_ISO_DATE); // 20140318
    String s2 = date.format(DateTimeFormatter.ISO_LOCAL_DATE); // 2014-03-18

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

    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类还支持一个静态工厂方法,它可以按照某个特定的模式创建DateTimeFormatter实例,代码清单如下:

    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
    LocalDate date1 = LocalDate.of(2014, 3, 18);
    String formattedDate = date1.format(formatter); // 13/03/2014
    LocalDate date2 = LocalDate.parse(formattedDate, formatter);

ofPattern方法也提供了一个重载的版本,使用它你可以创建某个Locale的格式器,代码清单如下所示:

    DateTimeFormatter italianFormatter = DateTimeFormatter.ofPattern("d. MMMM yyyy", Locale.ITALIAN);
    LocalDate date1 = LocalDate.of(2014, 3, 18);
    String formattedDate = date.format(italianFormatter); // 18. marzo 2014
    LocalDate date2 = LocalDate.parse(formattedDate, italianFormatter);

时区

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

时区是按照一定的规则将区域划分成的标准时间相同的区间。在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实例,它代表了相对于指定时区的时间点,代码清单如下所示:

    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);

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

新的日期时间API_第1张图片

通过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);

 

你可能感兴趣的:(Java,8)