本节继续探讨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 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 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的子类,可 以根据时区差构造。
可以认为,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 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方法,用于相对增加和减少时间。
可以为:
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);
可以为:
LocalDateTime ldt = LocalDateTime.now();
ldt = ldt.plusWeeks(1).with(ChronoField.DAY_OF_WEEK, 2)
.with(ChronoField.MILLI_OF_DAY, 0).withHour(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);
代码可以为:
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;
}