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表示时刻,获取当前时刻,代码为:
* * *
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有很多基于时刻的比较和计算方法,大多比较直观,我们就不列举了。
2.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是一个枚举,有7个取值,从DayOfWeek.MONDAY到DayOfWeek.SUN-DAY。
3.ZoneId/ZoneOffset
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的子类,可以根据时区差构造。
4.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由LocalDate和LocalTime构成,LocalDate加上时间可以构成LocalDate-Time,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));
* * *
5.ZonedDateTime
ZonedDateTime表示特定时区的日期和时间,获取系统默认时区的当前日期和时间,代码为:
* * *
ZonedDateTime zdt = ZonedDateTime.now();
* * *
LocalDateTime.now()也是获取默认时区的当前日期和时间,有什么区别呢?Local-DateTime内部不会记录时区信息,只会单纯记录年月日时分秒等信息,而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();
* * *
26.5.2 格式化
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);
* * *
26.5.3 设置和修改时间
修改时期和时间有两种方式,一种是直接设置绝对值,另一种是在现有值的基础上进行相对增减操作,Java 8的大部分类都支持这两种方式。另外,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);
* * *
3小时5分钟后,示例代码为:
* * *
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*1000)-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);
* * *
上面下周二指定是下周,如果是下一个周二呢?这与当前是周几有关,如果当前是周一,则下一个周二就是明天,而其他情况则是下周,代码可以为:
* * *
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);
* * *
26.5.4 时间段的计算
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();
* * *
26.5.5 与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; }
* * *
至此,关于Java 8的日期和时间API就介绍完了。相比以前版本的Date和Calendar,它引入了更多的类,但概念更为清晰,更为强大和易用。
本章介绍了Java 8引入的Lambda表达式、函数式编程,以及日期和时间API,利用本章介绍的内容,我们可以在更高的抽象层次上思考和解决问题,包括处理集合数据、管理异步任务、操作日期和时间等。