引言

Java 8之前的java.util.Date类以及其他用于建模日期时间的类有很多不一致及设计上的缺陷,包括易变性以及糟糕的偏移值、默认值和命名。 本篇博客将一起探索新的日期和时间API所提供的新特性。

LocalDate

该类的实例是一个不可变对象,它只提供了简单的日期,并不含当天的时间信息。另外,它也不附带任何与时区相关的信息。

我们可以通过静态工厂方法of创建一个LocalDate实例。LocalDate实例提供了多种方法来读取常用的值,比如年份、月份、星期几等,代码如下:

//构造日期(年,月,日)
LocalDate date = LocalDate.of(2014, 3, 18);
System.out.println(date);
//获取当前日期
LocalDate nowDate = LocalDate.now();
System.out.println(nowDate);
//获取年份
int year = nowDate.getYear();
System.out.println(year);
//获取月份
Month month = nowDate.getMonth();
System.out.println(month);
//获取日
int day = nowDate.getDayOfMonth();
System.out.println(day);
//获取星期
DayOfWeek dow = nowDate.getDayOfWeek();
System.out.println(dow);
//获取本月的天数
int len = nowDate.lengthOfMonth();
System.out.println(len);
//判断是否是闰年
boolean leap = nowDate.isLeapYear();
System.out.println(leap);

我们也可以通过传递一个TemporalField参数给get方法拿到同样的信息。

TemporalField是一个接口,它定义了如何访问temporal对象某个字段的值。ChronoField枚举实现了这一接口,所以我们可以很方便地使用get方法得到枚举元素的值,代码如下:

int year2 = date.get(ChronoField.YEAR);
System.out.println(year2);
int month2 = date.get(ChronoField.MONTH_OF_YEAR);
System.out.println(month2);
int day2 = date.get(ChronoField.DAY_OF_MONTH);
System.out.println(day2);
LocalTime

类似地,一天中的时间,比如13:45:20,可以使用LocalTime类表示。同LocalDate一样,LocalTime类也提供了一些getter方法访问这些变量的值,代码如下:

//构造时间(时、分、秒)
LocalTime time = LocalTime.of(13, 45, 20);
System.out.println(time);
//获取当前时间
LocalTime localTime = LocalTime.now();
System.out.println(localTime);
//获取时
int hour = localTime.getHour();
System.out.println(hour);
//获取分
int minute = localTime.getMinute();
System.out.println(minute);
//获取秒
int second = localTime.getSecond();
System.out.println(second);
LocalDateTime

LocalDateTime,是LocalDate和LocalTime的合体。它同时表示了日期和时间,但不带有时区信息,我们可以直接创建,也可以通过合并日期和时间对象构造,代码如下:

//2014-03-18T13:45:20
//构造日期时间(年、月、日、时、分、秒)
LocalDateTime dt1 = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45, 20);
System.out.println(dt1);
//构造日期(年、月、日)
LocalDate date = LocalDate.of(2014, 3, 18);
//构造时间(时、分、秒)
LocalTime time = LocalTime.of(13, 45, 20);
//构造日期时间(日期LocalDate、时间LocalTime)
LocalDateTime dt2 = LocalDateTime.of(date, time);
System.out.println(dt2);
//日期基础上构造时间(时、分、秒)
LocalDateTime dt3 = date.atTime(13, 45, 20);
System.out.println(dt3);
//日期基础上构造时间(LocalTime)
LocalDateTime dt4 = date.atTime(time);
System.out.println(dt4);
//时间基础上构造日期(LocalDate)
LocalDateTime dt5 = time.atDate(date);
System.out.println(dt5);

通过它们各自的atTime或者atDate方法,向LocalDate传递一个时间对象,或者向LocalTime传递一个日期对象的方式,我们可以创建一个LocalDateTime对象。

我们也可以使用 toLocalDate或者toLocalTime方法,从LocalDateTime中提取LocalDate或者LocalTime:

LocalDate date1 = dt1.toLocalDate();
System.out.println(date1);
LocalTime time1 = dt1.toLocalTime();
System.out.println(time1);
操纵和修改日期

如果有一个LocalDate对象,我们想要创建它的一个修改版,最直接也最简单的方法是使用withAttribute方法。withAttribute方法会创建对象的一个副本,并按照需要修改它的属性,代码如下:

//构造日期(年、月、日)
LocalDate date1 = LocalDate.of(2014, 3, 18);
System.out.println(date1);
//变更年
LocalDate date2 = date1.withYear(2011);
System.out.println(date2);
//变更日
LocalDate date3 = date2.withDayOfMonth(25);
System.out.println(date3);
//变更月
LocalDate date4 = date3.with(ChronoField.MONTH_OF_YEAR, 9);
System.out.println(date4);

注意,上面的代码中所有的方法都返回一个修改了属性的对象。它们都不会修改原来的对象!

另外,我们在项目中也经常会遇到需要对日期进行加减,API中为我们提供了其它方法,代码如下:

//构造日期(年、月、日)
LocalDate date5 = LocalDate.of(2014, 3, 18);
System.out.println(date5);
//加一周
LocalDate date6 = date5.plusWeeks(1);
System.out.println(date6);
//减3年
LocalDate date7 = date6.minusYears(3);
System.out.println(date7);
//加6个月
LocalDate date8 = date7.plus(6, ChronoUnit.MONTHS);
System.out.println(date8);

有的时候,我们可能需要进行一些更加复杂的操作,比如,将日期调整到下个周日、下个工作日,或者是本月的最后一天。

这时,我们可以使用重载版本的with方法,向其传递一个提供了更多定制化选择的TemporalAdjuster对象, 更加灵活地处理日期,代码如下:

//构造日期(年、月、日)
LocalDate date9 = LocalDate.of(2014, 3, 18);
System.out.println(date9);
//最近的一个周日,如当前日期为周日,则直接返回
LocalDate date10 = date9.with(nextOrSame(DayOfWeek.SUNDAY));
System.out.println(date10);
//本月最后一天
LocalDate date11 = date10.with(lastDayOfMonth());
System.out.println(date11);
解析和格式化日期

处理日期和时间对象时,格式化以及解析日期-时间对象是另一个非常重要的功能。新的 java.time.format包就是特别为这个目的而设计的。这个包中,重要的类是DateTimeFormatter。

下面使用DateTimeFormatter将一个日期转化为两种格式日期,代码如下:

//构造日期(年、月、日)
LocalDate date = LocalDate.of(2020, 2, 18);
System.out.println(date);
//格式化日期:20200218
String s1 = date.format(DateTimeFormatter.BASIC_ISO_DATE);
System.out.println(s1);
//格式化日期:2020-02-18
String s2 = date.format(DateTimeFormatter.ISO_LOCAL_DATE);
System.out.println(s2);

我们也可以使用parse方法,将两种格式的日期转为LocalDate,代码如下:

//2020-02-06
LocalDate date1 = LocalDate.parse("20200206",DateTimeFormatter.BASIC_ISO_DATE);
System.out.println(date1);
//2020-02-06
LocalDate date2 = LocalDate.parse("2020-02-06",DateTimeFormatter.ISO_LOCAL_DATE);
System.out.println(date2);

和老的java.util.DateFormat相比较,所有的DateTimeFormatter实例都是线程安全的。

利用DateTimeFormatter,我们可以将日期转换为想要的模式,如dd/MM/yyyy格式,代码如下:

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
//构造日期
LocalDate date3 = LocalDate.of(2020, 2, 18);
System.out.println(date3);
//转换为dd/MM/yyyy模式日期
String formattedDate = date3.format(formatter);
System.out.println(formattedDate);
//dd/MM/yyyy模式日期解析为LocalDate
LocalDate date4 = LocalDate.parse(formattedDate, formatter);
System.out.println(date4);
处理不同的时区

时区的处理是新版日期和时间API新增加的重要功能,使用新版日期和时间API时区的处理被极大地简化了。新的java.time.ZoneId 类是老版java.util.TimeZone的替代品。

时区是按照一定的规则将区域划分成的标准时间相同的区间。在ZoneRules这个类中包含了40个这样的实例。你可以简单地通过调用ZoneId的getRules()得到指定时区的规则。每个特定的ZoneId对象都由一个地区ID标识,比如:

ZoneId romeZone = ZoneId.of("Europe/Rome");

地区ID都为“{区域}/{城市}”的格式,一旦得到一个ZoneId对象,我们可以将它与LocalDate、LocalDateTime或者是Instant 对象整合起来,构造为一个ZonedDateTime实例,它代表了相对于指定时区的时间点,代码如下:

//构造时区
ZoneId romeZone = ZoneId.of("Europe/Rome");
//构造日期
LocalDate date = LocalDate.of(2020, Month.FEBRUARY, 18);
ZonedDateTime zdt1 = date.atStartOfDay(romeZone);
System.out.println(zdt1);
LocalDateTime dateTime = LocalDateTime.of(2020, Month.FEBRUARY, 18, 13, 45);
ZonedDateTime zdt2 = dateTime.atZone(romeZone);
System.out.println(zdt2);
Instant instant = Instant.now();
ZonedDateTime zdt3 = instant.atZone(romeZone);
System.out.println(zdt3);
总结

在我们的应用过程中,新的API的便利应该能有所体会,之前很多自己写的日期工具类都可以去掉了。除此之外,线程安全这一方面,我们也可以不用再自己单独处理了。