JavaSE 8—新的时间和日期API

jopen 10年前

为什么我们需要一个新的时间日期API

Java开发中一直存在一个问题,JDK提供的时间日期API一直对开发者没有提供良好的支持。

比如,已有的的类(如java.util.DateSimpleDateFormatter)不是线程安全的,会在并发情况下留下一些隐患,这不是开发者在编写处理日期的代码块时想要的效果。

某些日期时间处理类也表现出了相当不合理的设计,比如在java.util.Date类中,年从1900开始,月从1开始,日从0开始——就表现的不是很直观。

这些问题导致我们会选择一些第三方的日期时间处理库,比如:Joda-Time

为了在JDK中解决这些问题并提供更好的方式,Java SE8中设计了新的时间日期处理API。

这个项目为JSR-310并由Joda-Time(Stephen Colebourne)和Oracle共同设计,并会放在Java SE 8的java.time包下。

核心思路

这个新的API由三个核心思路组成:

  • 不可改变值的类。一个严重的问题是,对于Java中已经存在的格式化处理类(Formatter)不是线程安全的。这使开发人员在日常开发中需要编写线程安全的日期处理代码变得很麻烦。新的API保证所有的核心类中的值是不可变的,避免了并发情况下带来的不必要的问题。

  • 领域驱动设计。新的API模型可以精确的表示出DateTime的差异性。在以前的Java库中这一点就表现的非常差。比如,java.util.Date他表示一个时间点,从Unix时代开始就是以毫秒数的形式保存,但是你调用它的toString方法时,结果却显示它是有时区概念的,这就容易让开发者产生歧义。

    领域驱动设计的重点是从长远好处出发且简单易懂, 当你需要把你以前的处理时间的模块代码移植到Java8上时你就需要考虑一下领域模型的设计了。

  • 区域化时间体系。新的API允许人们在时区不同的时间体系下使用。比如日本或者泰国,他们不必要遵循 ISO-8601。新API为大多数开发者减少了很多额外的负担,我们只需要使用标准的时间日期API。

LocalDate类和LocalTime类

LocalDate类和LocalTime类很有可能是你使用新API时第一个遇见的类。他们本地化的原因是他们能够根据系统环境来表示日期和时间,就像放在桌上的日历或者挂在墙上的时钟。还有一个混合类是LocalDateLocalTime组合而成的,叫LocalDateTime

当你不知道你所运行环境的时区时,你应该使用这些本地化的类。比如桌面JavaFX程序就是其中之一。甚至可以在处于不同时区的分布式系统中使用这些类。

现有的时间日期API中的类都不是线程安全的,开发者需要处理潜在的并发问题——这不是大部分的开发者想要的。

创建对象

在新的API中所有的核心类都可以由工厂方法很方便的构建。当我通过某些类自身的字段来构建它时,可以使用of方法;当我通过从另外一个类型的转换来构建它时,可以使用from方法。同样也可以通过parse方法来由一个String参数构建它。参见代码1.
代码1

LocalDateTime timePoint = LocalDateTime.now(      );     // The current date and time  LocalDate.of(2012, Month.DECEMBER, 12); // from values  LocalDate.ofEpochDay(150);  // middle of 1970  LocalTime.of(17, 18); // the train I took home today  LocalTime.parse("10:15:30"); // From a String

在Java SE 8 中我们可以使用Java标准的getter方法来获取想要的值,参见代码2

代码2

LocalDate theDate = timePoint.toLocalDate();  Month month = timePoint.getMonth();  int day = timePoint.getDayOfMonth();  timePoint.getSecond();

你也可以对对象的值进行运算操作。因为新的API中所有的类型都是不可变的,他们都是调用了with方法并返回一个新对象,相当于使用了setter赋值。对于每一个字段都有提供了基本的运算方法。参见代码3

代码3

// Set the value, returning a new object  LocalDateTime thePast = timePoint.withDayOfMonth(      10).withYear(2010);    /* You can use direct manipulation methods,       or pass a value and field pair */  LocalDateTime yetAnother = thePast.plusWeeks(      3).plus(3, ChronoUnit.WEEKS);

新的API提供的一个调节器的概念–用来封装通用的处理逻辑的一段代码。你对任意时间使用WithAdjuster来设置一个或者多个字段,或者可以使用PlusAdjuster来对字段进行增量或者减法操作. 值类型也可以被当做调节器使用,用来更新字段的值. 新的API定义了一些内置的调节器, 但是如果你希望实现某些特定的业务逻辑,你也可以自己实现一个调节器. 参见代码4。

代码4

import static java.time.temporal.TemporalAdjusters.*;    LocalDateTime timePoint = ...  foo = timePoint.with(lastDayOfMonth());  bar = timePoint.with(previousOrSame(ChronoUnit.WEDNESDAY));    // Using value classes as adjusters  timePoint.with(LocalTime.now()); 

截取

新的API提供了表示不同精度的时间点类型来表示日期、时刻、日期+时刻。

API提供的truncatedTo方法适用于这种场景:他允许你从一个字段中截取出一个值。参见代码5。

代码5

LocalTime truncatedTime = time.truncatedTo(ChronoUnit.SECONDS);

时区

我们参考了以前的对于时区的很复杂的抽象方式。时区就是一个规则的集合,同样的时区遵守同样的时间标准规定,时区大约有40条规则。时区是由世界统一时间(UTC)来定义的。 他们基本上是同步的但是也有细小的差别。时区有两种命名定义: 简写型, 比如, “PLT”,完整型,“Asia/Karachi.”。当你设计程序的时候,你应该考虑是在什么时区下运行。

  • ZoneId 是时区的标示符. 每一个ZoneId都表示这些时区遵循同样的规则。 当你编码时你可以考虑使用例如“PLT”,“Asia/Karachi,”这种字符串来创建ZoneId。下面是一段完整的使用ZoneId的代码,参见代码6。

代码6

// You can specify the zone id when creating a zoned date time  ZoneId id = ZoneId.of("Europe/Paris");  ZonedDateTime zoned = ZonedDateTime.of(dateTime, id);  assertEquals(id, ZoneId.from(zoned));
  • ZoneOffset 是一段时间内代表格林尼治时间/世界同一时间与时区之间的差异。他能表示一个特殊的差异时区偏移量。参见代码7。

代码7

ZoneOffset offset = ZoneOffset.of("+2:00");

时区类

  • ZonedDateTime是一个带有时区的日期时间类。他能表示出与任意时区的某个时间点的时差. 如果你希望代表一个日期和时间不依赖特定服务器环境, 你就应该使用ZonedDateTime.

代码8

ZonedDateTime.parse("2007-12-03T10:15:30+01:00[Europe/Paris]");
  • OffsetDateTime 是一个带有偏移量的时间日期类。如果你的服务器处在不同的时区,他可以存入数据库中也可以用来记录某个准确的时间点。
  • OffsetTime 是一个带有偏移量的时间类。参见代码9
    代码9
OffsetTime time = OffsetTime.now();  // changes offset, while keeping the same point on the timeline  OffsetTime sameTimeDifferentOffset = time.withOffsetSameInstant(offset);  // changes the offset, and updates the point on the timeline  OffsetTime changeTimeWithNewOffset = time.withOffsetSameLocal(offset);  // Can also create new object with altered fields as before  changeTimeWithNewOffset.withHour(3).plusSeconds(2);

Java中已经存在了表示时区的类—java.util.TimeZone—但是他不能用在Java SE 8中,因为在JSR-310中所有的时间日期类是不可变的,时区类确是可变的。

Period类

Periods类用来表示例如“三个月零一天”这种描述一段时间的值。这是目前看来与其他类不同的表示一段时间而不是时间点的类。 参见代码10。

代码10

// 3 years, 2 months, 1 day  Period period = Period.of(3, 2, 1);    // You can modify the values of dates using periods  LocalDate newDate = oldDate.plus(period);  ZonedDateTime newDateTime = oldDateTime.minus(period);  // Components of a Period are represented by ChronoUnit values  assertEquals(1, period.get(ChronoUnit.DAYS)); 

Duration类

Duration类也是用来描述一段时间的, 他和Period类似,但是不同于Period的是,它表示的精度更细。参见代码11。

// A duration of 3 seconds and 5 nanoseconds  Duration duration = Duration.ofSeconds(3, 5);  Duration oneDay = Duration.between(today, yesterday);

我们可以对其进行加减或者with函数操作,也可以使用它来修改一个时间日期对象的值。

Java SE 8 的java.time包中新的时间日期API的功能可用性和安全性都大大提升了。 新的API很好用,可以适应大部分的场景。

Chronology系列类

由于我们需要支持无ISO日期年表表示的环境, Java SE 8 首次引入了Chronology类, 在这种环境下使用。 他们也实现了核心的时间日期接口。
Chronology:
– ChronoLocalDate
– ChronoLocalDateTime
– ChronoZonedDateTime

这些类应该使用在具有高度国际化的应用中需要使用本地化的时间体系时,没有这种需求的话最好不要使用它们。有一些时间体系中甚至没有一个月,一个星期这样的概念,我们只能通过更加通用抽象的字段进行运算。

其余的API改动

Java SE 8 还提供了一些其他场景下使用的类. MonthDay类表示一个月中的某几天,表示节日的时候可以使用。YearMonth类可以用来表示在比如信用卡的生效和失效时间(译者注:信用卡失效时间以月为单位)。

Java SE 8中也提供了对于JDBC的新的类型支持, 但是是一个非公开的修改。

这些类也可以和某些数据库类型进行映射。如下表,描述了这些类对于ANSI SQL的对应关系

ANSI SQL Java SE 8
DATE LocalDate
TIME LocalTime
TIMESTAMP TIMESTAMP
TIMESTAMP WITH TIMEZONE OffsetDateTime

总结

Java SE 8提供的java.time中新的日期和时间API。很大的提升了安全性,功能也更为强大。新的API架构模型,会让开发人员在各种各样场景下很方面的使用。

原文链接: oracle 翻译: ImportNew.com - 胡 劲寒
译文链接: http://www.importnew.com/9635.html