文章
Java
最初发表于 Java Magazine 2014 年 1 月/2 月刊。立即订阅。 |
作者:Ben Evans 和 Richard Warburton
为什么我们需要一个新的日期和时间库?
长期困扰 Java 开发人员的一个问题是对普通开发人员的日期和时间用例支持不足。
例如,现有的类(如 java.util.Date 和 SimpleDateFormatter)不是线程安全的,导致用户可能遇到并发问题 — 这不是普通开发人员编写日期处理代码时想要的结果。
有些日期和时间类还暴露出相当拙劣的 API 设计。例如,java.util.Date 中的年份从 1900 开始,月份从 1 开始,日期从 0 开始 — 不是很直观。
这些问题和其他一些问题导致出现了大量第三方日期和时间库,如 Joda-Time。
为了解决这些问题并在 JDK 核心中提供更好的支持,Java SE 8 设计了一个新的日期和时间 API,完全不存在这些问题。
根据 JSR 310,该项目由 Joda-Time 的作者 Stephen Colebourne 和 Oracle 共同领导,将出现在新的 Java SE 8 软件包 java.time 中。
新 API 源于三个核心思想:
领域驱动设计。新的 API 使用贴切代表 Date 和 Time 不同用例的类实现了精确的领域建模。以前的 Java 库在这方面的表现相当差。例如,java.util.Date 代表一个时间点,从 UNIX 时代开始就以毫秒数的形式保存,但您调用 toString() 时,结果却显示它有时区的概念,这就容易让开发人员产生歧义。
从长远来看,强调领域驱动设计的好处是条理清晰、易于理解,但从以前的 API 移植到 Java SE 8 时,您可能需要好好考虑应用程序的日期领域模型。
区分历法体系。新的 API 允许人们使用不同的历法体系,以支持世界上某些不一定遵循 ISO-8601 的地区(如日本或泰国)用户的需求。新的 API 不会给大多数开发人员增加额外的负担,他们只需要使用标准历法。
LocalDate 和 LocalTime 可能是您使用新 API 时最先遇到的类。它们实现了本地化,因为它们能够根据观察者环境来表示日期和时间,就像桌上的日历或墙上的时钟。还有一个名为 LocalDateTime 的组合类,由 LocalDate 和 LocalTime 组合而成。
现有的类不是线程安全的,导致用户可能遇到并发问题 — 这不是普通开发人员想要的结果。
时区可以消除不同观察者上下文环境的歧义,在此先放到一边;在不需要该上下文时,您应该使用这些本地类。桌面 JavaFX 应用程序可能就是其中之一。甚至可以在具有一致时区的分布式系统上使用这些类表示时间。
新 API 中的所有核心类均使用流畅的工厂方法构造。通过组成字段构造值时,工厂名为 of;从其他类型转换时,工厂名为 from。而且,还有些 parse 方法接受字符串作为参数。参见清单 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 getter 约定从 Java SE 8 类获取值,如清单 2 所示。
LocalDate theDate = timePoint.toLocalDate(); Month month = timePoint.getMonth(); int day = timePoint.getDayOfMonth(); timePoint.getSecond();
您还可以更改对象值来执行计算。因为新 API 中的所有核心类都是不可变的,这些方法称为 with,返回新对象,而不是使用 setter(参见清单 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。
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 提供了表示日期、时间以及日期和时间的类型来支持不同精度的时间点,但显然存在更细粒度的精度概念。
truncatedTo 方法的存在就是为了支持此类用例,它允许截取值给字段,如清单 5 所示。
LocalTime truncatedTime = time.truncatedTo(ChronoUnit.SECONDS);
前面讨论的本地类通过抽象消除了时区带来的复杂性。时区是一组规则,对应于一个使用相同标准时间的区域。大约有 40 个时区。时区是由相对协调世界时间 (UTC) 的偏移量定义的。它们按指定的时差大致同步移动。
可以用两种标识符来指代时区:缩写形式,例如“PLT”;完整形式,例如“Asia/Karachi”。设计应用程序时,应考虑哪些场合适合使用时区,什么时候适合使用偏移量。
ZoneId 是区域标识符(参见清单 6)。每个 ZoneId 对应于一些为该位置定义时区的规则。设计软件时,如果考虑使用“PLT”或“Asia/Karachi”等字符串,应使用此领域类。存储用户的时区偏好就是一个示例用例。
// 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));
清单 6
ZoneOffset 是代表格林威治时间/UTC 与某时区之间差异的时间段。这可以针对特定时刻的特定 ZoneId 来解析,如清单 7 所示。
ZoneOffset offset = ZoneOffset.of("+2:00");
清单 7
ZonedDateTime 是包含全限定时区的日期和时间(参见清单 8)。这可以解析任意时间点的偏移量。根据经验,如果希望表示一个日期和时间而不依赖特定服务器环境,应使用 ZonedDateTime。
ZonedDateTime.parse("2007-12-03T10:15:30+01:00[Europe/Paris]");
清单 8
OffsetDateTime 是包含解析偏移量的日期和时间。这有助于将数据序列化到数据库中,如果您有服务器处于不同的时区,还可以用它记录时间戳的序列化格式。
OffsetTime 是包含解析偏移量的时间,如清单 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);
清单 9
Java 中已有一个时区类 — java.util.TimeZone — 但不能用在 Java SE 8 中,因为所有 JSR 310 类都是不可变的,而时区是可变的。
Period 用于表示“三个月零一天”这种描述一段时间的值。这与我们目前为止所讨论的其他类不同,其他类只是一些时间点。参见清单 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));
清单 10
Java SE 8 将在 java.time 中包含一个新的日期和时间 API,为开发人员提供显著改进的安全性和功能。新的 API 擅长领域建模,提供了许多类,适用于各种开发人员用例的建模。
Duration 类也用来表示一段时间,与 Period 作用类似,但精度不同,如清单 11 所示。
// A duration of 3 seconds and 5 nanoseconds Duration duration = Duration.ofSeconds(3, 5); Duration oneDay = Duration.between(today, yesterday);
可以对 Duration 实例执行常规加、减和“with”操作,还可以使用 Duration 修改日期或时间的值。
为了支持使用非 ISO 历法系统的开发人员的需求,Java SE 8 引入了 Chronology 的概念,它代表一种历法体系,充当历法体系内时间点的工厂。还有些接口对应于核心时间点类,也实现了参数化
Chronology: ChronoLocalDate ChronoLocalDateTime ChronoZonedDateTime
这些类纯粹是为了开发高度国际化应用程序时需要考虑当地历法体系的开发人员而生,没有这些需求的开发人员最好不要使用它们。有些历法体系中甚至没有月或周的概念,因此需要通过非常通用的字段 API 来执行计算。
更多信息 |
Java SE 8 还有一些针对其他常见用例的类。MonthDay 类包含一对 Month 和 Day,用于表示生日。YearMonth 类可用来表示信用卡生效日期和失效日期,以及不具体指定哪天的约会场景。
Java SE 8 中的 JDBC 将支持这些新类型,但没有公开的 JDBC API 更改。现有的通用 setObject 和 getObject 方法就足够了。
这些类型可以映射到供应商特定的数据库类型或 ANSI SQL 类型;例如,ANSI 映射如表 1 所示。
| ANSI SQL | Java SE 8 |
| DATE | LocalDate |
| TIME | LocalTime |
| TIMESTAMP | LocalDateTime |
| TIME WITH TIMEZONE | OffsetTime |
| TIMESTAMP WITH TIMEZONE | OffsetDateTime |
Java SE 8 将在 java.time 中包含一个新的日期和时间 API,为开发人员提供显著改进的安全性和功能。新的 API 擅长领域建模,提供了许多类,适用于各种开发人员用例的建模。
| Ben Evans (@kittylyst) 是 jClarity 的 CEO,他还是伦敦 Java 社区 (LJC) 的组织者和 Java SE/EE 执行委员会的成员。 | |
| Richard Warburton 是一位经验丰富的技术专家,擅长解决深度技术问题。最近,他在 jClarity 从事高性能计算的数据分析工作。 |