前提
这篇文章主要介绍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)); } }
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)); 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()
方法:
public class Duration{
public static Duration between(Temporal startInclusive, Temporal endExclusive) }
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
提供的实例方法:
public class Period{
public long toTotalMonths() }
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() {}
public static TemporalAdjuster firstInMonth(DayOfWeek dayOfWeek) {}
public static TemporalAdjuster lastInMonth(DayOfWeek dayOfWeek) {}
public static TemporalAdjuster dayOfWeekInMonth(int ordinal, DayOfWeek dayOfWeek) {}
public static TemporalAdjuster next(DayOfWeek dayOfWeek) {}
public static TemporalAdjuster nextOrSame(DayOfWeek dayOfWeek) {}
public static TemporalAdjuster previous(DayOfWeek 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)