Java 8引入新的时间日期API相比于原来的API(Date和Calendar)的好处:

  1. 线程安全
  2. 更易于理解和使用
  3. 增加了时区考虑

下面简单介绍。时间的处理说简单也简单,说难也难,这里引用《Java核心技术卷2》中的两段话:

Time flies like an arrow, and we can easily set a starting point and count forward and backward in seconds. So why is it so hard to deal with time? The problem is humans. All would be easy if we could just tell each other: “Meet me at 1523793600, and don’t be late!” But we want time to relate to daylight and the seasons. That’s where things get complicated.

Time zones, perhaps because they are an entirely human creation, are even messier than the complications caused by the earth’s irregular rotation. In a rational world, we’d all follow the clock in Greenwich, and some of us would eat our lunch at 02:00, others at 22:00. Our stomachs would figure it out. This is actually done in China, which spans four conventional time zones. Elsewhere, we have time zones with irregular and shifting boundaries and, to make matters worse, the daylight savings time.

看来问题复杂化的根因还是人。

新API中比较常用的日期/时间分两大类:

  • LocalDate/LocalTime/LocalDateTime:这三种分别表示日期、时间、时间+日期,不含时区信息
  • ZonedDateTime/OffsetDateTime:表示时间+日期,带时区信息

下面用两张图来解释一下一些问题。下面是一个ISO-8601标准格式的完整时间:

datetime

这个时间可以拆成几部分,分别对应Java里面的类:

  • LocalDate:2008-08-08
  • LocalTime:20:00:00
  • ZoneOffset:+08:00
  • ZoneId:Asia/Shanghai
  • LocalDateTime:2008-08-08T20:00:00
  • OffsetDateTime:2008-08-08T20:00:00+08:00
  • ZonedDateTime:2008-08-08T20:00:00+08:00 Asia/Shanghai

这个已经很清楚了,就不赘述了,更详细的信息可以查看各个类的JavaDoc。那带时区和不带时区的区别在哪里?看下面的图:

timeline

时光如流水,一去不复返,时间永远是向前的。人为规定UTC时间1970-01-01T00:00:00Z为Epoch时间,此时定义Instant的值为0秒,往后加,往前减。即Instant代表从Epoch至今所经过的秒数,精度可以到纳秒。Epoch时间之前的Instant值自然就是负数,之后是正数。另外定义两个Instant之间的差值为Duration(类似的还有一个Period,表示两个LocalDate之间的差值)。很显然,Instant描述的是一个时间点,而且是个“绝对时间”:即不管你在地球的哪个角落,只要Instant值一样,那就表示的是时间线中的同一个时间。

同理,带有时区信息的时间也描述的是一个绝对时间,比如时间线上面的品红色时间点,对于中国人民来说是“2008-08-08T20:00+08 Asia/Shanghai”,对于巴黎人民来说是“2008-08-08T14:00+02:00 Europe/Paris”,但对应到时间线上面其实是同一个时间。然而不带时区信息的本地时间描述的则不是一个绝对时间,比如中国人的“当地时间2020年1月1日20:00点”和巴黎人民的“当地时间2020年1月1日20:00点”放在时间线上不是同一个时间点。

那到底该使用本地时间还是带时区信息的时间呢?一般来说,当你不需要描述时间线上面一个绝对时间的时候都应该优先使用本地时间,比如生日、节假日、计算差值、周期等场景,因为有了时区以后,问题往往会变的比较复杂。反之,如果你需要描述一个绝对时间,则应该用带有时区信息的时间,或者是Instant。

所以,可以看到新的API中既有方便操作的本地时间,也有考虑了时区的时间类型,即文章开头提到的好处3。另外,这些新的类(实例)都是immutable的,即和String类一样,当需要产生一个新对象的时候,都是重新new了一个实例,而不会修改原有对象,所以都是线程安全的。关于易用性,直接看一些常用的代码片段吧:

public class Test {
  public static void main(String[] args) throws Exception {
    // 计算某个操作的耗时
    Instant start = Instant.now();
    Thread.sleep(2000);
    Instant end = Instant.now();
    System.out.println(Duration.between(start, end).toMillis());
    Instant.now().plus(1, ChronoUnit.DAYS);

    // LocalDate
    LocalDate today = LocalDate.now();
    System.out.println(today);
    LocalDate tomorrow = today.plusDays(1);
    System.out.println(tomorrow);

    // 类似于Instant,Period表示两个LocalDate之间的差值
    Period period = Period.between(today, tomorrow);
    System.out.println(period.getDays());

    // 常用构造函数
    LocalDateTime now = LocalDateTime.now();
    System.out.println(now);
    LocalDateTime ldt1 = LocalDateTime.of(LocalDate.now(), LocalTime.now());
    LocalDateTime ldt2 = LocalDateTime.of(2008, 8, 8, 20, 0, 0);
    LocalDateTime ldt3 = LocalDateTime.ofInstant(Instant.now(), ZoneId.systemDefault());
    LocalDateTime ldt4 = LocalDateTime.parse("2008-08-08T20:00:00");
    LocalDateTime ldt5 = LocalDateTime.parse("2008-08-08 20:00:00", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));

    // 常用操作
    LocalDateTime localDateTime = LocalDateTime.now();
    localDateTime.plus(1, ChronoUnit.DAYS);
    localDateTime.plusHours(1);
    localDateTime.minus(1, ChronoUnit.HALF_DAYS);
    localDateTime.minusSeconds(10);
    localDateTime.isBefore(LocalDateTime.parse("2008-08-08T20:00:00"));
    localDateTime.isAfter(LocalDateTime.parse("2008-08-08T20:00:00"));
    localDateTime.compareTo(LocalDateTime.parse("2008-08-08T20:00:00"));

    // 时间格式化——使用内置的一些格式
    DateTimeFormatter dtf = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
    System.out.println(dtf.format(now));
    // 时间格式化——自定义格式
    System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));

    // 旧API向新API的转化,主要是通过Date和Calendar的toInstant()方法
    Date date = new Date();
    LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
    Calendar calendar = Calendar.getInstance();
    LocalDateTime.ofInstant(calendar.toInstant(), ZoneId.systemDefault());
  }
}

更多的使用到的时候Google,或者看JavaDoc即可。

总的来说:老API的功能新的全有,且更易用、更好用,所以没有任何理由继续使用旧的。

参考:

  • 《Core Java Volume II》