3.0 前言

不论做什么系统的开发,处理时间都是我们无法绕不过去的一道坎。

在 Java 世界里,java.util.Date 类应该是大多数人最早学习和使用的处理日期时间 API。这个类诞生于 Java 1.0 ,负责承载日期信息、日期之间的转换、不同日期格式的显示等多项功能,同时由于该类的设计过于简单粗暴,以至于其中的大部分方法在一年后发布的 Java 1.1 中就废弃了,并引入新的日历类 Calendar 作为部分功能的接班人,并明确了工作职责:

  • 使用Calendar类实现日期和时间字段之间转换;
  • 使用DateFormat类来格式化和分析日期字符串;
  • 而Date只用来承载日期和时间信息。

可惜的是,Calendar 类使用起来很复杂,同样没有被大众接受。

几个吐槽比较多的点:

1、Java的日期/时间类的定义并不一致,在 java.util 和 java.sql 的包中都有日期类,此外用于格式化和解析的类在 java.text 包中定义; 2、java.util.Date 同时包含日期和时间,而 java.sql.Date 仅包含日期,将其纳入 java.sql 包并不合理。另外这两个类都有相同的名字,这本身就是一个非常糟糕的设计; 3、api 难用,举一个简单的例子,还有更多比这个还难用的

Date date = new Date(2016,1,1);
System.out.println(date);   // 输出Thu Feb 01 00:00:00 CST 3916

输出结果的 年为3912,是2016+1900,而月,并不是我们给的月份参数,而是参数加1。

实际应用中,我们几乎都在使用一个开源的时间/日期库 Joda-Time,这个库流行到什么程度呢? Java 8 几乎原封不动的引入了这个库,这也是最受Java 开发者追捧的变化之一。下面就主要介绍一下来自Joda-Time 的新Java API,java.time包。

  • 扫盲: 1s = 10^3ms(毫秒) = 10^6μs(微秒) = 10^9ns(纳秒)

3.1 绝对时间

3.1.1 Instant

一个Instant对象代表时间轴上的一个点。原点(元年)被规定为1970年1月1日0点0分0秒,UNIX/POSIX 时间同样也是这样的约定。从原点开始,每天按照86400秒进行计算,向前向后分别以纳秒为单位。

静态方法 Instant.now() 会返回当前瞬间的时间点。

3.1.2 Duration

一个Duration对象表示两个瞬间(Instant)之间的时间量,一个常用的场景是计算某方法的执行时间:

Instant from = Instant.now();
// do something
Thread.sleep(1000);
Duration duration = Duration.between(from, Instant.now());
System.out.println(duration.toMillis());

3.1.3 解析

Instant(Duration)对象内部以一个 long 型变量维护秒值,而纳秒值由另一个int 保存。其中纳秒值是基于秒值的,所以纳秒值的int 变量永远不会大于999,999,999。

/**
 * The number of seconds from the epoch of 1970-01-01T00:00:00Z.
 */
private final long seconds;

/**
 * The number of nanoseconds, later along the time-line, from the seconds field.
 * This is always positive, and never exceeds 999,999,999.
 */
private final int nanos;

我们输出一个 Instant 对象的值来看一下,

Instant now = Instant.now();
System.out.println(now.getEpochSecond());  // 输出秒值 1460821922
System.out.println(now.getNano());   // 输出纳秒值 349000000

即当前时间时间轴上的时间点是从原点前进1460821922349000000纳秒的这个时刻。

3.2 本地日期/时间

使用绝对时间是最清晰、最简单不过的了。但是问题在于人,如果我们可以跟别人说:“1460821922 全体集合,别晚了”,是最能准备表达时间点的,可惜没有人理解的了。

3.2.1 LocalDate、LocalTime 和 LocalDateTime

这三个类联系很密切,可以一起介绍。LocalDate 表示一个日期,LocalTime 表示一天中的时间、LocalDateTime 包含了LocalDate 和 LocalTime 两部分,表示一个日期和时间。它们不关联任何时区信息。例如2015-08-27(用户端、兼职端上线日期)就是一个本地日期。由于没有时区信息,所以它无法与时间轴上一个准确的瞬时点对应。

你可以使用now 或 of 方法创建这三个类的实例,以日期为例:

LocalDate today = LocalDate.now();
LocalDate online = LocalDate.of(2015, 8, 27);
online = LocalDate.of(2015, Month.AUGUST, 27);

注意的是,Unix 和 java.util.Date 中年份从1900开始,月份从0开始。与之不符合生活的使用方式不同,现在我们可以使用人类习惯的表达方式,数字几就是几月份,即1表示一月份。

星期的表达上也有类似的区别,java.util.Calendar 中,每周从周日开始,即周六值为7, 周日值为1。 现在我们同样遵循人类表达方式,即每周从周一开始。

LocalDate wuyi = LocalDate.of(2016, 5, 1);
System.out.println(wuyi.getDayOfWeek());   // 输出 SUNDAY
System.out.println(wuyi.getDayOfWeek().getValue()); // 输出 7

3.2.2 Period

Period 与 Duration 类似,它表示一段逝去的年、月和日。在Period 内部使用三个变量维护这段逝去的时间,

/**
 * The number of years.
 */
private final int years;

/**
 * The number of months.
 */
private final int months;

/**
 * The number of days.
 */
private final int days;

所以,Period的 getYears、getMonths、getDays 方法实际获取的值是两个日期之间时段的一部分。

LocalDate wuyi = LocalDate.of(2016, 5, 1);
LocalDate shiyi = LocalDate.of(2016, 10, 1);
Period period = Period.between(wuyi, shiyi);

System.out.println(period.getYears());  // 输出 0
System.out.println(period.getMonths()); // 输出 5
System.out.println(period.getDays());   // 输出 0

要获取两个日期(只能是LocalDate)之间的天数,一般不会通过Period.between获取,因为这个方法返回的是几年几月几天,意义并不大。而是使用 until 方法, Period的between其实也是用的until 方法。

LocalDate wuyi = LocalDate.of(2016, 5, 1);
LocalDate shiyi = LocalDate.of(2016, 10, 1);
System.out.println(wuyi.until(shiyi, ChronoUnit.DAYS)); // 输出 153。即5.1到10.1有153天

3.3 时间调节器

时间调节器的工作已经基本被Qrartz、Spring Schedule这些框架给排挤的没有用武之地了。举例说一下,了解就好,有兴趣可以看TemporalAdjusters 的api。

例1:计算2016年1月第一个周二

LocalDate firstTuesday = LocalDate.of(2016, 1, 1).with(TemporalAdjusters.nextOrSame(DayOfWeek.TUESDAY));

例二:计算父亲节和母亲节 父亲节:每年六月的第三个星期日 母亲节:每年五月的第二个星期日

LocalDate faDay = LocalDate.of(2016, 6, 1).with(TemporalAdjusters.dayOfWeekInMonth(3, DayOfWeek.SUNDAY));
System.out.println(faDay);  // 2016-06-19

LocalDate maDay = LocalDate.of(2016, 5, 1).with(TemporalAdjusters.dayOfWeekInMonth(2, DayOfWeek.SUNDAY));
System.out.println(maDay); // 2016-05-08

3.4 有时区的时间

绝大多数应用场景下是不需要时区的,并且考虑时区会大大增加复杂性,比如处理夏令时。所以我们不需要也并不推荐使用时区时间。同样这里仅作简单介绍。

3.4.1 ZoneId、 ZoneDateTime

带时区时间的类是ZoneDateTime,还有一个重要的类是时区标识ZoneId类,我们可以直接通过ZoneDateTime.of(..省略年月日时分秒., ZoneId) 方法创建一个ZoneDateTime对象,也可以通过 LocalDateTime.now().atZone(ZoneId) 将一个本地类型转为带时区的类型。

3.4.2 OffsetDateTime

这个表示带有偏移量(基于UTC计算)的时间,但是没有时区规则。该类专门用于一些不需要时区规则的应用程序,比如某些网络协议。

3.5、格式化和解析

在Java 8 之前的版本,格式化时间我们会使用 SimpleDateFormat, Java 8 中与之对应的是 DateTimeFormatter,该类提供三种方式格式化日期时间

  • 预定义的标准格式 (比如 BA 认证用到的GMT格式即 RFC_1123_DATE_TIME 标准格式,详见API)
  • 语言环境相关的格式(没用,略)
  • 自定义格式(一般不会定义太多,我们只用到 yyyy-MM-dd HH:mm:ss)

贴一段代码简单看一下,更多使用方法可以参考 redscarf-common 的 DateUtils 类。

LocalDateTime now = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ISO_DATE_TIME;

// 格式化
String formatNow = formatter.format(now);
System.out.println(formatNow);  // 输出 2016-04-18T23:43:37.154

// 解析
LocalDateTime newNow = LocalDateTime.parse(formatNow, formatter);
System.out.println(newNow);    // 输出 2016-04-18T23:43:37.154

3.6 与遗留类互操作

列举几个典型的转换。如果用到其它类型间转换可以参考,然后自己尝试

/*
 * java.time.Instant 和 java.util.Date 互转
 */
Instant instant = Instant.now();
Date date = Date.from(instant);
instant = date.toInstant();

/*
 * java.time.Instant 和 java.sql.Timestamp 互转
 */
Timestamp timestamp = Timestamp.from(instant);
instant = timestamp.toInstant();

/*
 * java.time.LocalDateTime 和 java.sql.Timestamp 互转
 */
LocalDateTime localDateTime = LocalDateTime.now();
timestamp = Timestamp.valueOf(localDateTime);
localDateTime = timestamp.toLocalDateTime();

3.7 总结

3.7.1 格式对比

一张图片总结一下各种类型的格式:

3.7.2 一点强调

所有类型都是不可变的,不管什么操作,总会返回一个新的实例,原实例不变

3.7.3 一个记忆的小技巧

java.time 包的 API 提供了大量相关的方法,这些方法一般有一致的方法前缀:

of:静态工厂方法。 parse:静态工厂方法,关注于解析。 get:获取某些东西的值。 is:检查某些东西的是否是true。 with:不可变的setter等价物。 plus:加一些量到某个对象。 minus:从某个对象减去一些量。 to:转换到另一个类型。 at:把这个对象与另一个对象组合起来,例如: date.atTime(time)。

3.7.4 最后的总结

这一部分内容难度很小,虽然写的内容挺多,但是也没做什么真正讲解说明,更多的是体系和 API 的介绍。目的是明确几个重要类的概念和作用,让我们在开发中用的更顺手。