前提

这篇文章主要介绍JSR-310中日期时间类的常用计算工具,包括常规的两个日期时间实例之间的前后比较、间隔的时间量等等。

日期时间的基准类

日期时间类库中提供了几个常用的计算或者度量基准类,分别是:

  • 表示取值范围的ValueRange:内部持有四个主要的成员变量minSmallest、minLargest、maxSmallest和maxLargest,可以表示的值范围是[minSmallest/maxSmallest,minLargest/maxLargest]
  • 表示秒和纳秒级别的时间量DurationTemporalAmount的实现类,内部持有一个长整型的成员seconds代表秒和一个整型的成员nanos代表纳秒,由秒和纳秒组成时间量。
  • 表示年月日级别的时间量PeriodTemporalAmount的实现类,内部持有三个整型的成员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.SECONDSChronoUnit.NANOS,而Period只支持ChronoUnit.YEARSChronoUnit.MONTHSChronoUnit.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)