过去,在Java中处理日期和时间时,无外乎这两个类(以及它们的子类)

  • java.util.Date
  • java.util.Calendar

但它们固有一些缺陷,到了Java8,我们有了更多更好的选择,包括又不限于

  • java.time.Instant
  • java.time.Duration
  • java.time.format.DateTimeFormatter

绝对时间

如果我们把时间看做一道长河(时间轴),那么其上的每一个,就是一个瞬间(Instant),用java.time.Instant类的对象表示。Instant类可以表示的时间范围非常之广,从-1000000000-01-01 00:00:00到+1000000000-12-31 23:59:59.999999999,和先前的java.util.Date类似,仍然取1970-01-01 00:00:00作为时间的纪元,这3个特殊的时间点,可以通过Instant.MIN,Instant.MAX和Instant.EPOCH加以验证。若要取得当前的瞬时值,可以使用now方法。

1
2
3
4
5
6
7
8
9
10
//获得Instant可以表示的最小、最大时间点
Instant min = Instant.MIN;
Instant max = Instant.MAX;
System.out.println(min);
System.out.println(max);
//获取Instant时间纪元和当前时间点
Instant epoch = Instant.EPOCH;
Instant now = Instant.now();
System.out.println(epoch);
System.out.println(now);

两个Instant之间的距离,被称为持续时间(Duration),要计算Duration,使用between方法,然后再通过toNanos|Seconds|Days等方法得到这段时间对应的,从纳秒到天,不同精度的表示。

1
2
3
4
5
//计算两个Instant之间的Duration
Duration duration = Duration.between(epoch, now);
System.out.println(duration);
System.out.println(duration.toMillis());
System.out.println(duration.toDays());

无论是Instant对象还是Duration对象,它们都是不可变的,底层分实际上由一个long型和一个int型整数组成,前者表示这个时间点距离时间纪元的数,后者表示一个最多可以精确到纳秒的调整值,注意这里已经不再是距离时间纪元的毫秒数了。

此外,二者均提供了诸多数学操作,即包括了做四则运算的plus|minus[Nanos|Seconds|Days]multiplied|dividedBy,也有类似absisZero|Negative这样的方法。

1
2
3
4
5
6
7
8
9
10
//对Instant和Duration进行数学操作
Instant nowPlusDays = now.plus(1, ChronoUnit.DAYS);
System.out.println(nowPlusDays);
Instant nowMinusSeconds = now.minusSeconds(86400);
System.out.println(nowMinusSeconds);
Duration durationMultiplyFactor = duration.multipliedBy(3);
System.out.println(durationMultiplyFactor.toMillis());
Duration durationDivideFactor = duration.dividedBy(2);
System.out.println(durationDivideFactor.toMillis());
System.out.println(duration.isNegative());

本地日期/时间

Java8提供了LocalDate、LocalTime、LocalDateTime这么3个类来表示本地日期/时间。用这3个类所表示的时间,是无法对应在Instant上的,主要原因是它缺少了时区等信息,也就无法准确地与一个瞬时点对应起来。

可以使用nowof方法构造LocalDate、LocalTime、LocalDateTime对象,先前Date类的几个奇怪设计:月份取值0-11、年份是目标年份与1900之差等都不复存在了,而且类似于Instant和Duration,提供了plus|minus[Days|Weeks|Months|Years]等方法可用于数学计算。此外,两个LocalDateTime只差不再是Duration,而是时段(Period)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//构造本地日期/时间
LocalDate nowDate = LocalDate.now();
LocalTime nowTime = LocalTime.now();
LocalDateTime ofDateTime = LocalDateTime.of(2017, 1, 1, 8, 0, 0, 0);
System.out.println(nowDate);
System.out.println(nowTime);
System.out.println(ofDateTime);
//数学计算
LocalDate nowDateMinusYears = nowDate.minusYears(1);
System.out.println(nowDateMinusYears);
System.out.println(nowDateMinusYears.isLeapYear());
//计算两个LocalDateTime之间的Period
LocalDate springFestival = LocalDate.of(2017, 1, 1);
LocalDate nationalDay = LocalDate.of(2017, 10, 1);
Period period = springFestival.until(nationalDay);
System.out.println(period);
long periodInDays = springFestival.until(nationalDay, ChronoUnit.DAYS);
System.out.println(periodInDays);

如果要获取/设置已经用LocalDate、LocalTime、LocalDateTime表示的某个时间中某个字段(如:年、月、日、时、分、秒)的值,对应的也有get|withYear|Minute等方法(略好奇为什么设置不是set而是with……)。

1
2
3
4
5
6
7
//字段值操作
Month month = nowDate.getMonth();
DayOfWeek dayOfWeek = nowDate.getDayOfWeek();
System.out.println(month);
System.out.println(dayOfWeek);
LocalTime withTime = nowTime.withHour(0).withMinute(0).withSecond(0).withNano(0);
System.out.println(withTime);

对LocalDate而言,with方法还有一种特殊的用法,就是通过日期校正器(TemporalAdjusters),直接计算出first|lastDayOf(Next)Month|Year(今年/本月/次月/明年的第一天/最后一天)、first/last/dayOfweekInMonth(本月中的第一个/最后一个/第X个星期X)、next|previous(OrSame)(下一个/上一个(或同一个)星期X)。

1
2
3
4
5
6
7
8
9
10
11
12
//使用TemporalAdjusters
LocalDate aDate = LocalDate.of(2017, 2, 1);
LocalDate firstDayOfNextMonth = aDate.with(TemporalAdjusters.firstDayOfNextMonth());
LocalDate lastDayOfYear = aDate.with(TemporalAdjusters.lastDayOfYear());
LocalDate firstFridayInMonth = aDate.with(TemporalAdjusters.firstInMonth(DayOfWeek.FRIDAY));
LocalDate nextOrSameWednesday = aDate.with(TemporalAdjusters.nextOrSame(DayOfWeek.WEDNESDAY));
LocalDate previousWednesday = aDate.with(TemporalAdjusters.previous(DayOfWeek.WEDNESDAY));
System.out.println(firstDayOfNextMonth);
System.out.println(lastDayOfYear);
System.out.println(firstFridayInMonth);
System.out.println(nextOrSameWednesday);
System.out.println(previousWednesday);

带时区的日期时间

虽说时区完全就是一个人为的概念,但带有时区的时间反而更符合实际情况,Java8用ZonedDateTime来表示这种时间。

首先,它可以和LocalDateTime一样直接构造出来,无非是多了一个表示时区ZoneId作为参数。其次,它也可以从LocalDateTime转化而来,或者转化为LocalDateTime,用的是LocalDateTime#atZoneZonedDateTime#toLocalDateTime方法。

1
2
3
4
5
6
7
LocalDateTime localDateTime = LocalDateTime.of(2017, 1, 1, 0, 0, 0, 0);
ZonedDateTime fromLocalDateTime = localDateTime.atZone(ZoneId.of("UTC"));
System.out.println(fromLocalDateTime);
ZonedDateTime ofZonedDateTime = ZonedDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneId.of("UTC"));
System.out.println(ofZonedDateTime);
LocalDateTime toLocalDateTime = ofZonedDateTime.toLocalDateTime();
System.out.println(toLocalDateTime);

ZonedDateTime和上面其他所有类一样,也提供了非常多用于数学计算的方法,当然由于涉及时区、夏令时等的计算,应当在实现上会更复杂,不过在中国很少开发涉及这些的程序,我也就没有过多去关注了。

格式化和解析

个人觉得格式化和解析这一段同以前的java.text.DateFormat和java.text.SimpleDateFormat类似乎没有太大的变化,同样可以把一个日期/时间做/从3种不同形式的格式化/解析,分别是标准格式、本地化格式和自定义格式

1
2
3
4
5
6
7
8
9
10
11
12
13
LocalDateTime now = LocalDateTime.now();
//标准格式
String isoLocalDateTime = DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(now);
System.out.println(isoLocalDateTime);
//本地化格式
String fullLocalizedDateTime = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).format(now);
System.out.println(fullLocalizedDateTime);
//自定义格式
String patternDateTime = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS").format(now);
System.out.println(patternDateTime);
//解析
ZonedDateTime zonedDateTime = ZonedDateTime.parse("2017-01-01T00:00:00.000+08:00[Asia/Shanghai]",DateTimeFormatter.ISO_ZONED_DATE_TIME);
System.out.println(zonedDateTime);