26.5 Java 8的日期和时间API

26.5 Java 8的日期和时间API

本节介绍Java 8对日期和时间API的增强。我们在之前介绍了Java 8以前的日期和时间API,主要的类是Date和Calendar,由于它的设计有一些不足,Java 8引入了一套新的API,位于包java.time下。本节我们就来简要介绍这套新的API,先从日期和时间的表示开始。

26.5.1 表示日期和时间

我们在第7章介绍过日期和时间的几个基本概念,包括时刻、时区和年历,这里就不赘述了。Java 8中表示日期和时间的类有多个,主要的有:

  • Instant:表示时刻,不直接对应年月日信息,需要通过时区转换;
  • LocalDateTime:表示与时区无关的日期和时间,不直接对应时刻,需要通过时区转换;
  • ZoneId/ZoneOffset:表示时区;
  • LocalDate:表示与时区无关的日期,与LocalDateTime相比,只有日期,没有时间信息;
  • LocalTime:表示与时区无关的时间,与LocalDateTime相比,只有时间,没有日期信息;
  • ZonedDateTime:表示特定时区的日期和时间。

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

1. Instant

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

1
Instant now = Instant.now();

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

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

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

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

转换Instant为Date,代码为:

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

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

2. LocalDateTime

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

1
LocalDateTime ldt = LocalDateTime.now();

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

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

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

1
2
3
4
5
6
public int getYear()
public int getMonthValue()
public int getDayOfMonth()
public int getHour()
public int getMinute()
public int getSecond()

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

1
public DayOfWeek getDayOfWeek()

DayOfWeek是一个枚举,有7个取值,从DayOfWeek.MONDAY到DayOfWeek.SUN-DAY。

3. ZoneId/ZoneOffset

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

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

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

1
public ZonedDateTime atZone(ZoneId zone)

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

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

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

4. LocalDate/LocalTime

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

1
2
3
4
5
6
7
8
//表示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加上时间可以构成LocalDate-Time, LocalTime加上日期可以构成LocalDateTime,比如:

1
2
3
4
5
6
7
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));

5. ZonedDateTime

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

1
ZonedDateTime zdt = ZonedDateTime.now();

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

1
2
3
4
//根据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,比如:

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

26.5.2 格式化

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

1
2
3
4
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));

输出为:

1
2016-08-18 14:20:45

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

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

26.5.3 设置和修改时间

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

调整时间为下午3点20分,代码为:

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

还可以为:

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

3小时5分钟后,示例代码为:

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

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

今天0点,可以为:

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

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

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

LocalTime.MIN表示”00:00”。也可以为:

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

下周二上午10点整,可以为:

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

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

1
2
3
4
5
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,这是一个函数式接口,定义为:

1
2
3
public interface TemporalAdjuster {
Temporal adjustInto(Temporal temporal);
}

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

1
public static TemporalAdjuster next(DayOfWeek dayOfWeek)

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

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

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

1
2
3
4
5
6
7
8
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中还有很多方法,部分方法如下:

1
2
3
4
5
6
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)

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

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

或者为:

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

本月最后一天最后一刻,代码可以为:

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

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

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

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

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

下个月第一个周一的下午5点整,代码可以为:

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

26.5.4 时间段的计算

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

1
2
3
4
5
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
1年3月18天

根据生日计算年龄,示例代码可以为:

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

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

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

26.5.5 与Date/Calendar对象的转换

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

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

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

1
2
3
4
5
6
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,代码可以为:

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

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

1
2
3
4
5
6
public static ZonedDateTime toZonedDateTime(Calendar calendar) {
ZonedDateTime zdt = ZonedDateTime.ofInstant(
Instant.ofEpochMilli(calendar.getTimeInMillis()),
calendar.getTimeZone().toZoneId());
return zdt;
}

至此,关于Java 8的日期和时间API就介绍完了。相比以前版本的Date和Calendar,它引入了更多的类,但概念更为清晰,更为强大和易用。

本章介绍了Java 8引入的Lambda表达式、函数式编程,以及日期和时间API,利用本章介绍的内容,我们可以在更高的抽象层次上思考和解决问题,包括处理集合数据、管理异步任务、操作日期和时间等。