过去,在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 min = Instant.MIN; Instant max = Instant.MAX; System.out.println(min); System.out.println(max); 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
| 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,也有类似abs、isZero|Negative这样的方法。
1 2 3 4 5 6 7 8 9 10
| 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上的,主要原因是它缺少了时区等信息,也就无法准确地与一个瞬时点对应起来。
可以使用now、of方法构造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()); 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
| 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#atZone和ZonedDateTime#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);
|