关于java.time这个包,应该来说已经有不少的教程和文章了。不过最近处理了很多日期相关的问题,修改了项目中的时间日期工具类,之后又将整个工具类用新的time包重写。总结一下发布在此,展示一些常规操作。

总体简介

真要好好了解的话还是应该去看整个包的javadoc。不过这里还是要从为何要增加这个新的日期-时间API来说起。java对于时间的处理最早伊始于java.util.Date类,然后是jdk1.1开始使用的java.util.Calendar类,最后直到现在的java.time。

更换到Calendar类(以及DateFormat类)的主要原因是Date类包含了日期信息的存储、转换、显示,承载的职责过于复杂。更换到time包的主要原因是日期设置不够友好、时区支持不够好而且属性均可变导致线程不安全。这些也间接说明了新的time API的几个优秀特质:日期时间分离、各个类职责明确、线程安全、完善的时区支持等。

java.time包大量吸收了优秀的第三方库Joda-Time的内容,因此相关操作和Joda-Time非常相似。

基本类和接口

Instant

时间线上的瞬时点,可以通俗地认为就是时间戳,底层是对System.currentTimeMillis()的封装,主要由自1970-01-01零点为基准的以long表示的秒数和以int表示的时刻纳秒数(0 - 999,999,999)两部分组成。在与Date类进行转换是时经常需要用到。

LocalDate

表示不带时间的本地日期,time包将日期与时间分离,使得很多操作更加简便。

LocalTime

表示不带日期的本地时间。

LocalDateTime

表示本地日期时间,本质上由LocalDate和LocalTime组成,使用频率会比较高。

ChronoUnit

这是个枚举类,代表了标准的日期-时间单位。包含了纳秒,秒,分钟,周,年,世纪等一共16个类型。相关的标准接口为TemporalUnit。常用于获取日期和时间的某个属性。

ChronoField

同样是枚举类,代表了标准的用于操作日期和时间的领域(不知道怎么翻译更好)。包含了分钟的秒、小时的分钟、一天的小时等等,虽然翻译地不太好,但是原有的Calendar类也是有相同概念的内容的,只要看到了应该不难理解。相关的标准接口为TemporalField。常用于日期的计算、变换。

Duration

代表了时间的间隔,持有的内容精确到纳秒,常用于计算时间差。

Period

代表了日期的间隔,以年月日来表示。也可以使用相关LocalDate类中的until方法来达到相同的计算效果。

时区、位移相关

虽然时区处理是time包的优势,但这部分用到的不多,我本人也不是特别熟,就简单罗列一下。时区相关主要有ZonedDateTime、ZoneId、Clock等,位移相关主要有OffsetTime、OffsetDateTime、ZoneOffset等。

DateTimeFormatter

时间格式化处理类,用于时间的格式化和解析,这个类本身带有大量的静态标准值提供快速使用。

TemporalAdjuster

首先这是一个接口,表示时间的调整、计算,下面的示例有很大一部分与它有关,使用时可以配合lambda进行更加简洁优雅的表示。包内提供了一个标准的实现类TemporalAjusters,直接给予了许多通用的时间变换方法,尤其是针对星期这一方面。

使用示例

下面分类展示一些日期处理的典型代码。

与java.util.Date的转换

1
2
3
4
5
6
7
8
9
10
// Date转LocalDate
LocalDate localDate = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
// Date转LocalTime
LocalTime localTime = date.toInstant().atZone(ZoneId.systemDefault()).toLocalTime();
// Date转LocalDateTime
LocalDateTime localDateTime = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
// LocalDate转Date(取零点)
Date date = Date.from(localDate.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant());
// LocalDateTime转Date
Date date = Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());

时间日期的构造

1
2
3
4
5
6
7
8
9
10
11
// 获取当前的日期和时间
Instant now = Instant.now();
LocalDate currentDate = LocalDate.now();
LocalTime currentTime = LocalTime.now();
LocalDateTime currentDateTime = LocalDateTime.now();
// 指定年月日时分秒纳秒
LocalDate date = LocalDate.of(year, month, dayOfMonth);
LocalTime time = LocalTime.of(hour, minute, second, nanoOfSecond);
LocalDateTime localDateTime = LocalDateTime.of(year, month, dayOfMonth, hour, minute, second, nanoOfSecond);
// 以ISO标准日期时间解析生成
LocalDateTime localDateTime = LocalDateTime.parse("2007-12-03T10:15:30");

获取相关属性

1
2
3
4
5
6
7
8
9
10
11
12
// 获取年、月、日
localDate.getYear();
localDate.getMonthValue();
localDate.getDayOfMonth();
// 获取小时、分钟、秒
localTime.getHour();
localTime.getMinute();
localTime.getSecond();
// 判断闰年
localDate.isLeapYear();
// 获取所在月份的天数
localDate.range(DAY_OF_MONTH).getMaximum();

日期时间调整运算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 增加日期的天数
localDate.plusDays(days);
localDate.plus(days, ChronoUnit.DAYS);
// 增加分钟
localTime.plusMinutes(minutes);
localTime.plus(minutes, ChronoUnit.MINUTES);
// 增加年月日
localDate.with(temporal -> temporal
.plus(years, ChronoUnit.YEARS)
.plus(months, ChronoUnit.MONTHS)
.plus(days, ChronoUnit.DAYS));
// 分秒归零
localDateTime.with(temporal -> temporal
.with(ChronoField.MINUTE_OF_HOUR,0)
.with(ChronoField.SECOND_OF_MINUTE,0)
.with(ChronoField.MILLI_OF_SECOND,0));
// 取小时的尾时间点
localDateTime.with(temporal -> temporal
.plus(Duration.ofHours(1))
.with(ChronoField.MINUTE_OF_HOUR,0)
.with(ChronoField.SECOND_OF_MINUTE,0)
.with(ChronoField.MILLI_OF_SECOND,0)
.minus(Duration.ofMillis(1)));
// 调整日期为当月1号
localDate.with(TemporalAdjusters.firstDayOfMonth());
// 调整日期为下月1号
localDate.with(TemporalAdjusters.firstDayOfNextMonth());
// 调整日期为上月月末
localDate.with(temporal -> {
Temporal tmp = temporal.minus(1, MONTHS);
long maxdays = tmp.range(DAY_OF_MONTH).getMaximum();
return tmp.with(DAY_OF_MONTH, maxdays);
});

日期时间比较、间隔

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 比较相等,主类均重写了equal方法
localDate.equals(other);
localTime.equals(other);
localDate.isEqual(other)
// 比较前后
localDate.isBefore(other)
localDate.isAfter(other)
// 时间间隔,相差的秒数以及纳秒数
Duration duration = Duration.between(localDateTime, other);
duration.getSeconds();
duration.getNano();
// 时间间隔统计
duration.toNanos();
duration.toHours();
duration.toDays();
// 日期间隔,相差的年月日
Period period = Period.between(LocalDate, other);
period.getYears();
period.getMonths();
period.getDays();
// 日期间隔统计
period.toTotalMonths();

日期格式化

1
2
3
4
5
// 标准格式
DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(localDateTime);
// 自定义格式
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
String strDate = formatter.format(localDateTime);

小结

经过这几个常用的简短示例,相信可以快速建立对新的time API的理解。对于这个时间包,最重要的就是理解上述的基本类和接口,所有的操作都围绕这些展开。

时区相关的内容本篇暂不讨论,但是时区是一个比较容易出问题的地方,生产环境一定要注意系统的时区设置,防止发生错误。此外,对于星期相关的处理可以参考TemporalAdjusters提供的相关实现。