在 Java 中,想处理日期和时间时,通常都会选用 java.util.Date
这个类进行处理。不过不知道是设计者在当时没想好还是其它原因,在 Java 1.0 中引入的这个类,大部分的 API 在 Java 1.1 中就被标记为了 Deprecated(已过时),而这些标记为已过时的接口大部分都是一些 getter 和 setter,它们被移到了 java.util.Calendar
和 java.text.DateFormat
这些类里面。这样就出现了我想操作日期和时间,结果需要同时操作好几个类,给编程带来了麻烦。除此之外,java.util.Date
本身还有设计上的缺陷。诸如月份从 0 开始啦、年份从 1900 年开始推算(JavaScript 也是这个尿性),外部可以随意更改等。为了解决这些痛点,也出现了一些第三方的工具包帮助开发者,在 Java 8 中,一组新的日期与时间 API (JSR-310)被引入进来,解决了上面的种种问题。接下来,将简要介绍这一组 API,并给出我自己的一些使用建议。
JSR-310(日期与时间 API)简介
Java 8 中引入的这一套新的 API 位于 java.base
模块,java.time
包下。官方文档对这一组 API 的描述如下:
The main API for dates, times, instants, and durations.
The classes defined here represent the principle date-time concepts, including instants, durations, dates, times, time-zones and periods. They are based on the ISO calendar system, which is the de facto world calendar following the proleptic Gregorian rules. All the classes are immutable and thread-safe.
简单地说,就是把我们能想到的所有对时间的相关操作都包含进来了。这些日期/时间/时刻/时段类的实体都是不可改变(immutable)的,对它们的任何修改都将产生一个新的对象。因此也是线程安全的。
在这个包下常用的一些类/枚举列举如下
| 描述 |
| 表示时间轴上的一个时刻。与 |
| 表示(不带时区的)日期/时间 |
| 定义了一组表示星期几的常量(e.g. |
| 定义了一组表示月份的常量(e.g. |
在 API 的命名上,该包下的 API 命名遵循如下规则:
-
of
- static factory method -
parse
- static factory method focussed on parsing -
get
- gets the value of something -
is
- checks if something is true -
with
- the immutable equivalent of a setter -
plus
- adds an amount to an object -
minus
- subtracts an amount from an object -
to
- converts this object to another type -
at
- combines this object with another, such asdate.atTime(time)
另外,根据 MyBatis 文档的说明,从 3.4.5 开始,MyBatis 默认支持 JSR-310(日期和时间 API),所以用上述的日期与时间 API 也是可以用来操作数据库中的时间数据的。其它的 ORM 框架(Hibernate 等)应该也提供了对这一套 API 的支持。
对于 Android 开发者来说,印象中是直到 API Level 28 才能够使用 JSR-310 这一套 API,所以对于 Android 开发者来说,使用第三方的日期时间库可能是更妥当的选择(出于应用程序的向下兼容)
使用例子
这里举一个我在开发考试系统过程中的例子。读者也可以通过阅读这篇文章获取更加完整的 API 使用的示例。
在考生登录考试系统中,需要设定他的考试开始时间和结束时间,其中开始时间默认为现在,结束时间由开始时间加上试卷上的考试时间(以分钟为单位)得到。
考试记录的实体类的部分代码如下:
@TableName("t_exam_record")
public class ExamRecordEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 考试记录ID
*/
@TableId("id")
private Long examRecordId;
/**
* 计划开始时间
*/
@TableField("plan_start_time")
private Instant planStartTime;
/**
* 计划结束时间
*/
@TableField("plan_end_time")
private Instant planEndTime;
/**
* 实际开始时间
*/
@TableField("actual_start_time")
private Instant actualStartTime;
/**
* 实际结束时间
*/
@TableField("actual_end_time")
private Instant actualEndTime;
// 省略 getter、setter
}
其中当考生开始考试后,需要设置 planStartTime
、planEndTime
和 actualStartTime
为相应的值。如果这里使用 java.util.Date
,那么除了直接用 new
创建一个实体会比较方便以外,其它的操作就变得相当麻烦了。而如果用上新式 API,只要以下几行代码即可完成:
/**
* 考试开始,保存考试记录
* @param limitTime 考试时长(分钟为单位)
* @return 考试记录的 DTO,用于后续处理
*/
private MobilePaperDTO saveNewRecord(int limitTime) {
ExamRecordEntity entity = new ExamRecordEntity();
Instant currentTime = Instant.now();
entity.setPlanStartTime(currentTime);
entity.setActualStartTime(currentTime);
entity.setPlanEndTime(currentTime.add(limitTime, ChronoUnit.MINUTES));
// 省略其它操作
}
其中 ChronoUnit
是预先定义好的一组时间单位。由于 Instant
代表的是时间轴上的一个点,只能加减上一个“时间段”(在 java.time.temporal
包下有相关定义)。如果这里选择使用 LocalDateTime
保存日期和时间,则可直接使用 LocalDateTime.plusMinutes()
方法。Java 并不支持运算符重载,不然在某些支持运算符重载的语言(例如 Kotlin)上,这套 API 可以表现的更优雅一些。
开发建议
就我个人的使用见解来说,这部分新的 API 肯定是越早引入越好。如果是老旧系统或者和别的不熟悉这套 API 的开发者协同开发,建议直接使用 Instant
,因为这个就是官方用来取代 Date
的类,并且与 Date
间可以相互转化,之后再慢慢引入其它 API。