计 算机程序的思维逻辑 (95) - Java 8的日期和时间API


​本节继续探讨Java 8的新特性,主要是介绍Java 8对日期和时间API的增强,关于日期和时间,我们在之前已经介绍过两节了, 32 节 介绍了Java 1.8以前的日期和时间API,主要的类是Date和Calendar,由于它的设计有一些不足,业界广泛使用的是一个第三方的类库Joda-Time,关于Joda-time,我们在 33 节 进行了介绍。Java 1.8学习了Joda-time,引入了一套新的API,位于包java.time下,本节,我们就来简要介绍这套新的API。

我们先从日期和时间的表示开始。

表示日期和时间

基 本概念

我们在 32 节 介绍过日期和时间的几个基本概念,这里简要回顾下。

Java 8中表示日期和时间的类有多个,主要的有:

类比较多,但概念更为清晰了,下面我们逐个来看 下。

Instant

Instant表示时刻,获取当前时刻,代码 为:

Instant now = Instant.now();

可以根据Epoch Time (纪元时)创建Instant,比如,另一种获取当前时刻的代码可以为:

Instant now = Instant.ofEpochMilli(System.currentTimeMillis());

我们知道,Date也表示时刻,Instant 和Date可以通过纪元时相互转换,比如,转换Date为Instant,代码为:

public static Instant toInstant(Date date) {
    return Instant.ofEpochMilli(date.getTime());
}

转换Instant为Date,代码为:

public static Date toDate(Instant instant) {
    return new Date(instant.toEpochMilli());
}

Instant有很多基于时刻的比较和计算方 法,大多比较直观,我们就不列举了。

LocalDateTime

LocalDateTime表示与时区无关的日 期和时间信息,获取系统默认时区的当前日期和时间,代码为:

LocalDateTime ldt = LocalDateTime.now();

还可以直接用年月日等信息构建 LocalDateTime,比如,表示2017年7月11日20点45分5秒,代码可以为:

LocalDateTime ldt = LocalDateTime.of(2017, 7, 11, 20, 45, 5);

LocalDateTime有很多方法,可以获 取年月日时分秒等日历信息,比如:

public int getYear()
public int getMonthValue()
public int getDayOfMonth()
public int getHour()
public int getMinute()
public int getSecond()

还可以获取星期几等信息,比如:

public DayOfWeek getDayOfWeek() 

DayOfWeek是一个枚举,有七个取值,从 DayOfWeek.MONDAY到DayOfWeek.SUNDAY。

LocalDateTime不能直接转为时刻 Instant,转换需要一个参数ZoneOffset,ZoneOffset表示相对于格林尼治的时区差,北京是+08:00,比如,转换一个LocalDateTime为北京的时刻,方法为:

public static Instant toBeijingInstant(LocalDateTime ldt) {
    return ldt.toInstant(ZoneOffset.of("+08:00"));
}

给定一个时刻,使用不同时区解读,日历信息是不 同的,Instant有方法根据时区返回一个ZonedDateTime:

public ZonedDateTime atZone(ZoneId zone)

默认时区是 ZoneId.systemDefault(),可以这样构建ZoneId:

//北京时区
ZoneId bjZone = ZoneId.of("GMT+08:00")

ZoneOffset是ZoneId的子类,可 以根据时区差构造。

LocalDate/LocalTime

可以认为,LocalDateTime由两部分 组成,一部分是日期LocalDate,另一部分是时间LocalTime,它们的用法也很直观,比如:

//表示2017年7月11日
LocalDate ld = LocalDate.of(2017, 7, 11);

//当前时刻按系统默认时区解读的日期
LocalDate now = LocalDate.now();

//表示21点10分34秒
LocalTime lt = LocalTime.of(21, 10, 34);

//当前时刻按系统默认时区解读的时间
LocalTime time = LocalTime.now();

LocalDateTime由 LocalDate和LocalTime构成,LocalDate加上时间可以构成LocalDateTime,LocalTime加上日期可以构成LocalDateTime,比如:

LocalDateTime ldt = LocalDateTime.of(2017, 7, 11, 20, 45, 5);
LocalDate ld = ldt.toLocalDate(); //2017-07-11
LocalTime lt = ldt.toLocalTime(); // 20:45:05

//LocalDate加上时间,结果为2017-07-11 21:18:39
LocalDateTime ldt2 = ld.atTime(21, 18, 39);

//LocalTime加上日期,结果为2016-03-24 20:45:05
LocalDateTime ldt3 = lt.atDate(LocalDate.of(2016, 3, 24));

ZonedDateTime

ZonedDateTime表示特定时区的日期 和时间,获取系统默认时区的当前日期和时间,代码为:

ZonedDateTime zdt = ZonedDateTime.now();

LocalDateTime.now()也是获 取默认时区的当前日期和时间,有什么区别呢? LocalDateTime内部不会记录时区信息,只会单纯记录年月日时分秒等信息,而ZonedDateTime除了记录日历信息,还 会记录时区 ,它的其他大部分构建方法都需要显式传递时区,比如:

//根据Instant和时区构建ZonedDateTime
public static ZonedDateTime ofInstant(Instant instant, ZoneId zone)

//根据LocalDate, LocalTime和ZoneId构造
public static ZonedDateTime of(LocalDate date, LocalTime time, ZoneId zone) 

ZonedDateTime可以直接转换为 Instant,比如:

ZonedDateTime ldt = ZonedDateTime.now();
Instant now = ldt.toInstant();

格式化/解析字符串

Java 8中,主要的格式化类是java.time.format.DateTimeFormatter,它是线程安全的,看个例子:

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime ldt = LocalDateTime.of(2016,8,18,14,20,45);
System.out.println(formatter.format(ldt));

输出为:

2016-08-18 14:20:45

将字符串转化为日期和时间对象,可以使用对应类 的parse方法,比如:

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String str = "2016-08-18 14:20:45";
LocalDateTime ldt = LocalDateTime.parse(str, formatter);

设置和修改时间

修改时期和时间有两种方式,一种是直接设置绝对 值,另一种是在现有值的基础上进行相对增减操作,Java 8的大部分类都支持这两种方式,另外,与Joda-Time一样, Java 8的大部分类都是不可变类,修改操作是通过创建并返回新对象来实现的,原对象本身不会变。

我们来看一些例子。

调整时间为下午3点20

代码示例为:

LocalDateTime ldt = LocalDateTime.now();
ldt = ldt.withHour(15).withMinute(20).withSecond(0).withNano(0);

还可以为:

LocalDateTime ldt = LocalDateTime.now();
ldt = ldt.toLocalDate().atTime(15, 20);

三 小时五分钟后

示例代码为:

LocalDateTime ldt = LocalDateTime.now();
ldt = ldt.plusHours(3).plusMinutes(5);

LocalDateTime有很多 plusXXX和minusXXX方法,用于相对增加和减少时间。

今 天0点

可以为:

LocalDateTime ldt = LocalDateTime.now();
ldt = ldt.with(ChronoField.MILLI_OF_DAY, 0);      

ChronoField是一个枚举,里面定义了 很多表示日历的字段,MILLI_OF_DAY表示在一天中的毫秒数,值从0到(24 * 60 * 60 * 1,000) - 1。

还可以为:

LocalDateTime ldt = LocalDateTime.of(LocalDate.now(), LocalTime.MIN);

LocalTime.MIN表示"00:00"

也可以为:

LocalDateTime ldt = LocalDate.now().atTime(0, 0);

下 周二上午10点整

可以为:

LocalDateTime ldt = LocalDateTime.now();
ldt = ldt.plusWeeks(1).with(ChronoField.DAY_OF_WEEK, 2)
    .with(ChronoField.MILLI_OF_DAY, 0).withHour(10);

下 一个周二上午10点整

上面下周二指定是下周,如果是下一个周二呢?这 与当前是周几有关,如果当前是周一,则下一个周二就是明天,而其他情况则是下周,代码可以为:

LocalDate ld = LocalDate.now();
if(!ld.getDayOfWeek().equals(DayOfWeek.MONDAY)){
    ld = ld.plusWeeks(1);
}
LocalDateTime ldt = ld.with(ChronoField.DAY_OF_WEEK, 2).atTime(10, 0);

针对这种复杂一点的调整,Java 8有一个专门的接口TemporalAdjuster,这是一个函数式接口,定义为:

public interface TemporalAdjuster {
    Temporal adjustInto(Temporal temporal);
}

Temporal是一个接口,表示日期或时间对 象,Instant,LocalDateTime,LocalDate等都实现了它,这个接口就是对日期或时间进行调整,还有一个专门的类TemporalAdjusters,里面提供了很多TemporalAdjuster的实现, 比如,针对下一个周几的调整,方法是:

public static TemporalAdjuster next(DayOfWeek dayOfWeek)

针对上面的例子,代码可以为:

LocalDate ld = LocalDate.now();
LocalDateTime ldt = ld.with(TemporalAdjusters.next(DayOfWeek.TUESDAY)).atTime(10, 0);

这个next方法是怎么实现的呢?看代码:

public static TemporalAdjuster next(DayOfWeek dayOfWeek) {
    int dowValue = dayOfWeek.getValue();
    return (temporal) -> {
        int calDow = temporal.get(DAY_OF_WEEK);
        int daysDiff = calDow - dowValue;
        return temporal.plus(daysDiff >= 0 ? 7 - daysDiff : -daysDiff, DAYS);
    };
}

它内部封装了一些条件判断和具体调整,提供了更 为易用的接口。

TemporalAdjusters中还有很多 方法,部分方法如下:

public static TemporalAdjuster firstDayOfMonth()
public static TemporalAdjuster lastDayOfMonth()
public static TemporalAdjuster firstInMonth(DayOfWeek dayOfWeek)
public static TemporalAdjuster lastInMonth(DayOfWeek dayOfWeek)
public static TemporalAdjuster previous(DayOfWeek dayOfWeek)
public static TemporalAdjuster nextOrSame(DayOfWeek dayOfWeek)

这些方法的含义比较直观,就不解释了,它们主要 是封装了日期和时间调整的一些基本操作,更为易用。

明 天最后一刻

代码可以为:

LocalDateTime ldt = LocalDateTime.of(LocalDate.now().plusDays(1), LocalTime.MAX);

或者为:

LocalDateTime ldt = LocalTime.MAX.atDate(LocalDate.now().plusDays(1));

本 月最后一天最后一刻

代码可以为:

LocalDateTime ldt =  LocalDate.now()
        .with(TemporalAdjusters.lastDayOfMonth())
        .atTime(LocalTime.MAX);

lastDayOfMonth()是怎么实现的 呢?看代码:

public static TemporalAdjuster lastDayOfMonth() {
    return (temporal) -> temporal.with(DAY_OF_MONTH, temporal.range(DAY_OF_MONTH).getMaximum());
}        

这里使用了range方法,从它的返回值可以获 取对应日历单位的最大最小值,展开来,本月最后一天最后一刻的代码还可以为:

long maxDayOfMonth = LocalDate.now().range(ChronoField.DAY_OF_MONTH).getMaximum();
LocalDateTime ldt =  LocalDate.now()
        .withDayOfMonth((int)maxDayOfMonth)
        .atTime(LocalTime.MAX);

下 个月第一个周一的下午5点整

代码可以为:

LocalDateTime ldt = LocalDate.now()
        .plusMonths(1)
        .with(TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY))
        .atTime(17, 0);       

时间段的计算

Java 8中表示时间段的类主要有两个,Period和Duration,Period表示日期之间的差,用年月日表示,不能表示时间,Duration表示时间差,用时分秒表等表示,也可以用天表示,一天严格等于24小时,不能用年月表示,下面看一些例子。

计 算两个日期之间的差

看个Period的例子:

LocalDate ld1 = LocalDate.of(2016, 3, 24);
LocalDate ld2 = LocalDate.of(2017, 7, 12);
Period period = Period.between(ld1, ld2);
System.out.println(period.getYears() + "年"
        + period.getMonths() + "月" + period.getDays() + "天");

输出为:

1年3月18天

根 据生日计算年龄

示例代码可以为:

LocalDate born = LocalDate.of(1990,06,20);
int year = Period.between(born, LocalDate.now()).getYears();

计 算迟到分钟数

假定早上9点是上班时间,过了9点算迟到,迟到 要统计迟到的分钟数,怎么计算呢?看代码:

long lateMinutes = Duration.between(
        LocalTime.of(9,0),
        LocalTime.now()).toMinutes(); 

与Date/Calendar对象的转换

Java 8的日期和时间API没有提供与老的Date/Calendar相互转换的方法,但在实际中,我们可能是需要的,前面介绍了,Date可以与Instant通过毫秒数相互转换,对于其他类型,也可以通过毫秒数/Instant相互转换。

比如,将LocalDateTime按默认时区 转换为Date,代码可以为:

public static Date toDate(LocalDateTime ldt){
    return new Date(ldt.atZone(ZoneId.systemDefault())
            .toInstant().toEpochMilli());
}

将ZonedDateTime转换为 Calendar,代码可以为:

public static Calendar toCalendar(ZonedDateTime zdt) {
    TimeZone tz = TimeZone.getTimeZone(zdt.getZone());
    Calendar calendar = Calendar.getInstance(tz);
    calendar.setTimeInMillis(zdt.toInstant().toEpochMilli());
    return calendar;
}

Calendar保持了 ZonedDateTime的时区信息。

将Date按默认时区转为 LocalDateTime,代码可以为:

public static LocalDateTime toLocalDateTime(Date date) {
    return LocalDateTime.ofInstant(
            Instant.ofEpochMilli(date.getTime()),
            ZoneId.systemDefault());
}

将Calendar转为 ZonedDateTime,代码可以为:

public static ZonedDateTime toZonedDateTime(Calendar calendar) {
    ZonedDateTime zdt = ZonedDateTime.ofInstant(
            Instant.ofEpochMilli(calendar.getTimeInMillis()),
            calendar.getTimeZone().toZoneId());
    return zdt;
}