前提
这篇文章主要介绍JSR-310中日期时间类的常用计算工具,包括常规的两个日期时间实例之间的前后比较、间隔的时间量等等。
日期时间的基准类
日期时间类库中提供了几个常用的计算或者度量基准类,分别是:
- 表示取值范围的
ValueRange
:内部持有四个主要的成员变量minSmallest、minLargest、maxSmallest和maxLargest,可以表示的值范围是[minSmallest/maxSmallest,minLargest/maxLargest]
。 - 表示秒和纳秒级别的时间量
Duration
:TemporalAmount
的实现类,内部持有一个长整型的成员seconds代表秒和一个整型的成员nanos代表纳秒,由秒和纳秒组成时间量。 - 表示年月日级别的时间量
Period
:TemporalAmount
的实现类,内部持有三个整型的成员years、months和days分别代表年、月、日,由年月日组成时间量。 - 日期时间的基本单位
TemporalUnit
:主要实现类是枚举类型ChronoUnit
,一个ChronoUnit
成员会维护一个字符串名字属性name和一个Duration
类型的实例。 - 日期时间的属性(field)表示
TemporalField
:主要实现是枚举类型ChronoField
,一个ChronoField
成员会维护一个字符串名字属性name、一个TemporalUnit
的基础单位baseUnit、一个TemporalUnit
的表示范围的单位rangeUnit和一个ValueRange
类型的range用于表示当前属性的范围。
举一些简单的使用例子:
public class ValueRangeMain {
public static void main(String[] args) throws Exception {
ValueRange valueRange = ValueRange.of(1L, 10000L);
System.out.println(valueRange);
valueRange = ValueRange.of(1L, 5L, 10000L, 50000L);
System.out.println(valueRange);
}
}
// 输出结果
1 - 10000
1/5 - 10000/50000
public class DurationMain {
public static void main(String[] args) throws Exception {
Duration duration = Duration.of(1L, ChronoUnit.HOURS);
System.out.println(duration);
duration = Duration.from(duration);
System.out.println(duration);
duration = Duration.ofSeconds(1L, 999_999_999);
System.out.println(duration.get(ChronoUnit.SECONDS));
}
}
//输出结果 - toString方法重写了,有特定的格式
PT1H
PT1H
1
public class PeriodMain {
public static void main(String[] args) throws Exception {
Period period = Period.of(10, 10, 10);
System.out.println(period);
period = Period.from(period);
System.out.println(period.getYears());
}
}
//输出结果
P10Y10M10D
10
常用计算工具
判断是否闰年
判断是否闰年这个功能是由年表Chronology
提供的,因为不同的年表中的闰年规则可能不一致。一般情况下,我们都是使用ISO规范下的年表,对应的是IsoChronology
,可以看一下IsoChronology
判断闰年方法的实现:
public boolean isLeapYear(long prolepticYear) {
return ((prolepticYear & 3) == 0) && ((prolepticYear % 100) != 0 || (prolepticYear % 400) == 0);
}
这个也是最常见的Java基础面试题之一,可以记下来怎么实现。静态方法java.time.Year#isLeap()
也是同样的实现。举个简单的使用例子:
public class IsLeapYearMain {
public static void main(String[] args) throws Exception {
int year = 2016;
System.out.println(Year.isLeap(year));
System.out.println(IsoChronology.INSTANCE.isLeapYear(year));
// 2018年
LocalDate localDate = LocalDate.now();
LocalDateTime localDateTime = LocalDateTime.now();
System.out.println(localDate.isLeapYear());
System.out.println(localDateTime.toLocalDate().isLeapYear());
}
}
// 输出结果
true
true
false
false
比较日期时间的先后
所有的日期时间、日期、时间类都具备三个比较方法:isBefore()
、isAfter()
和isEqual()或者equals()
,对于ChronoLocalDateTime
或者ChronoZonedDateTime
,底层总是先转化为新纪元天数再基于天数进行比较。举个简单的使用例子:
public class DateTimeCompareMain {
public static void main(String[] args) throws Exception {
System.out.println(LocalDateTime.now().isBefore(LocalDateTime.now().plus(1, ChronoUnit.SECONDS)));
System.out.println(LocalDate.now().isBefore(LocalDate.now().plus(1, ChronoUnit.DAYS)));
System.out.println(LocalTime.now().equals(LocalTime.now().plus(1, ChronoUnit.SECONDS)));
}
}
// 输出
true
true
false
计算日期时间的间隔
计算日期时间的间隔主要通过Duration
或者Period
的静态方法,主要是通过两个类的between()
方法:
// Duration中
public class Duration{
public static Duration between(Temporal startInclusive, Temporal endExclusive)
}
// Period中
public class Period{
public static ChronoPeriod between(ChronoLocalDate startDateInclusive, ChronoLocalDate endDateExclusive)
public static Period between(LocalDate startDateInclusive, LocalDate endDateExclusive)
}
对于日期时间类来说,计算时间间隔底层是基于TemporalUnit#between()
方法,入口方法一般是long until(Temporal endExclusive, TemporalUnit unit)
方法。
举个简单的使用例子:
public class DurationPeriodMain {
public static void main(String[] args) throws Exception {
LocalTime start = LocalTime.of(1, 1, 1);
LocalTime end = LocalTime.of(2, 2, 2);
Duration duration = Duration.between(start, end);
long until = start.until(end, ChronoUnit.SECONDS);
System.out.println(duration.getSeconds());
System.out.println(until);
LocalDateTime startDt = LocalDateTime.of(2017, 9, 6, 1, 2, 3);
LocalDateTime endDt = LocalDateTime.of(2018, 1, 6, 12, 12, 12);
duration = Duration.between(startDt, endDt);
until = startDt.until(endDt, ChronoUnit.SECONDS);
System.out.println(duration.getSeconds());
System.out.println(until);
LocalDate startD = LocalDate.of(2018, 2, 1);
LocalDate endD = LocalDate.of(2019, 1, 6);
Period period = Period.between(startD, endD);
Period untilPeriod = startD.until(endD);
System.out.println(period);
System.out.println(untilPeriod);
}
}
// 输出结果
3661
3661
10581009
10581009
P11M5D
P11M5D
只要通过计算得到Duration
或者Period
实例,那么可以通过get(TemporalUnit unit)
方法转换为对应单位的时间量,但是要注意的是对于此方法Duration
只支持ChronoUnit.SECONDS
和ChronoUnit.NANOS
,而Period
只支持ChronoUnit.YEARS
、ChronoUnit.MONTHS
和ChronoUnit.DAYS
。一般情况下,我们更希望得知两个日期时间之间相差多少年,多少个月等,这个时候,可以使用Duration
或者Period
提供的实例方法:
// Period中
public class Period{
// 相差的总月数
public long toTotalMonths()
}
// Period中
public class Period{
// 转换为天数
public long toDays()
// 转换为小时数
public long toHours()
// 转换为分钟数
public long toMinutes()
// 转换为秒钟数
public long toSeconds()
// 转换为毫秒数
public long toMillis()
// 转换为纳秒数
public long toNanos()
// 转换为准确天数
public long toDaysPart()
// 转换为准确小时数
public int toHoursPart()
// 转换为准确分钟数
public int toMinutesPart()
// 转换为准确秒钟数
public int toSecondsPart()
// 转换为准确毫秒数
public int toMillisPart()
// 转换为准确纳秒数
public int toNanosPart()
}
以上的实例方法都是基于整数的除法,也就是说会截断尾数。举个简单使用例子:
LocalDateTime start = LocalDateTime.of(2017, 9, 6, 1, 2, 3);
LocalDateTime end = LocalDateTime.of(2018, 1, 6, 12, 12, 12);
Duration duration = Duration.between(start, end);
Period period = Period.between(start.toLocalDate(), end.toLocalDate());
System.out.println(duration.toDays());
System.out.println(period.toTotalMonths());
// 输出结果
122
4
如果不使用Duration
或者Period
,可以直接使用日期时间类的util()
方法,本质是一致的,以LocalDateTime
为例:
LocalDateTime start = LocalDateTime.of(2017, 9, 6, 1, 2, 3);
LocalDateTime end = LocalDateTime.of(2018, 1, 6, 12, 12, 12);
long months = start.until(end, ChronoUnit.MONTHS);
long days = start.until(end, ChronoUnit.DAYS);
System.out.println(days);
System.out.println(months);
// 输出结果
122
4
日期校准器
日期校准器TemporalAdjuster
定义了特定的规则基于输入的基础日期时间对象,通过校准规则计算,得到最终的校准结果。TemporalAdjusters
中定义了一系列可以直接使用的的返回TemporalAdjuster
实例的公有静态工厂方法。例如:
public final class TemporalAdjusters {
......
// 校准到对应月份的第一天
public static TemporalAdjuster firstDayOfMonth() {}
// 校准到对应月份的最后一天
public static TemporalAdjuster lastDayOfMonth() {}
// 校准到对应月份的下个月的第一天
public static TemporalAdjuster firstDayOfNextMonth() {}
// 校准到对应年份的第一天
public static TemporalAdjuster firstDayOfYear() {}
// 校准到对应年份的最后一天
public static TemporalAdjuster lastDayOfYear() {}
// 校准到对应年份的下一年的第一天
public static TemporalAdjuster firstDayOfNextYear() {}
// 校准到对应月份的第一个星期N
public static TemporalAdjuster firstInMonth(DayOfWeek dayOfWeek) {}
// 校准到对应月份的最后一个星期N
public static TemporalAdjuster lastInMonth(DayOfWeek dayOfWeek) {}
// 校准到对应月份的第ordinal个星期N
public static TemporalAdjuster dayOfWeekInMonth(int ordinal, DayOfWeek dayOfWeek) {}
// 校准到下一个星期N
public static TemporalAdjuster next(DayOfWeek dayOfWeek) {}
// 校准到下一个星期N,如果当前日期时间对象满足dayOfWeek,则返回自身
public static TemporalAdjuster nextOrSame(DayOfWeek dayOfWeek) {}
// 校准到上一个星期N
public static TemporalAdjuster previous(DayOfWeek dayOfWeek) {}
// 校准到上一个星期N,如果当前日期时间对象满足dayOfWeek,则返回自身
public static TemporalAdjuster nextOrSame(DayOfWeek dayOfWeek) {}
......
}
举几个简单的例子(笔者更新这个章节的日期是2020-03-01
,星期天):
public class Main {
static DateTimeFormatter F = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) throws Exception {
OffsetDateTime time = OffsetDateTime.now();
OffsetDateTime temp = time.with(TemporalAdjusters.firstDayOfMonth());
System.out.println(String.format("校准到%d月的第一天:%s", time.getMonthValue(), temp.format(F)));
temp = time.with(TemporalAdjusters.lastDayOfMonth());
System.out.println(String.format("校准到%d月的最后一天:%s", time.getMonthValue(), temp.format(F)));
temp = time.with(TemporalAdjusters.firstDayOfYear());
System.out.println(String.format("校准到%d年的第一天:%s", time.getYear(), temp.format(F)));
temp = time.with(TemporalAdjusters.lastDayOfYear());
System.out.println(String.format("校准到%d年的最后一天:%s", time.getYear(), temp.format(F)));
time.with(TemporalAdjusters.firstInMonth(DayOfWeek.FRIDAY));
System.out.println(String.format("校准到%d月的第一个星期一:%s", time.getMonthValue(), temp.format(F)));
temp = time.with(TemporalAdjusters.lastInMonth(DayOfWeek.SUNDAY));
System.out.println(String.format("校准到%d月的最后一个星期天:%s", time.getMonthValue(), temp.format(F)));
}
}
// 输出结果
校准到3月的第一天:2020-03-01 16:53:50
校准到3月的最后一天:2020-03-31 16:53:50
校准到2020年的第一天:2020-01-01 16:53:50
校准到2020年的最后一天:2020-12-31 16:53:50
校准到3月的第一个星期一:2020-12-31 16:53:50
校准到3月的最后一个星期天:2020-03-29 16:53:50
小结
善用内置的日期时间工具,多数场景下能事半功倍。JSR-310
提供的日期时间API
和附加工具已经足够强大,熟练使用可以摆脱第三方时间日期处理框架的依赖。
(本文完 c-1-d e-a-201816 r-a-20200301)