早在Java 8 之前,JDK 为我们提供了 Date 和 Calendar 这两个类来操作日期和时间,从使用角度来看,这两个类的 API 设计简直是反人类,所以在我们开始讲解新API之前,我们先看看已有的 API 存在什么问题,请看下面的代码:
1 2 |
|
你能说出上面的代码会打印出什么吗?很多程序员会说 “0012-12-12”,但实际上会打印出“Sun Jan 12 00:00:00 IST 1913”,是不是与期望的相差很多,这说明这个 API 从设计上来说对程序员很不友好,使用起来容易产生很多不必要的麻烦,下面我们来分析下上面的代码都有哪些问题:
- 代码中每个 12 都是什么意思?是年月日、日月年还是其他的组合?
- 月份是从 0 开始的,如果想要设置为 12 月那么参数应该写成 11,当写成 12 时又会从 1 月开始,所以打印的结果是 1 月
- 现有的 API 年份是从 1900 年开始计算的,而月份写成 12 后会进位,所以打印的结果年份是 1900 + 12 + 1 = 1913
- 我们在 new Date() 的过程中只传入了年月日,而打印出来的还有时分秒以及时区,这些信息对我们来说都是多余的
此外,用于解析、格式化日期或时间的 DateFormat 类只能处理 Date 类型,无法处理时间类型且还是非线程安全的,上面只是列举了几个反例而已,实际使用起来还有不少坑要踩,总之用两个字来总结,那就是——难用!
由于存在这些难用的日期 API,大家伙儿都转到了第三方库上,比如 Joda-Time。这让 Oracle 颜面有些挂不住了,于是奋发图强,找来了一批牛人,在吸取了 Joda-Time 的不少设计上的优点以及分析了以前 API 中存在的问题后重新设计了一套原生日期时间 API,这些 API 都位于 java.time 包下。设计者们在设计这些 API 时应用了 领域驱动(domain-driven design)和不可变(Immutability)的设计原则使得 API 简洁容易理解。注意,由于所有位于 java.time 包下的核心类都是不可变的,也就是说对这些类的对象进行的所有操作都会创建一个新的对象,并不会修改原有对象的值,这就避免了线程安全的问题。下面我们将通过代码快速讲解如何去使用它们。
LocalDate, LocalTime, LocalDateTime, Instant, Duration, Period
上面这 6 个类是日期时间 API 的核心类,掌握了它们相当于学会了这套 API 的一半。
LocalDate 对象只能用于表示日期,即年、月、日,时分秒都表示不了,并且也不保存任何时区相关的信息。创建 LocalDate 的实例可以通过调用 LocalDate 的 4 个静态工厂方法:
1 2 3 4 5 |
|
前 2 个方法是传入的是年月日,只不过第 2 个方法使用了 Month 枚举,让代码更易读,前两个方法的效果都是一样的。第 3 个方法用于获取某年的第几天的日期,比如 2016 年第 59 天是 2016-02-28;第 4 个方法是计算从 Unix 元年( 1970-01-01)开始后第几天的日期,如 Unix 元年后的第 1000 天的日期是 1972-09-27;第 5 个方法是通过系统时钟来获取当期的日期,很便捷。
1 2 3 4 |
|
LocalDate 自身也提供了一些很便利的方法供我们使用:
1 2 3 4 5 6 7 8 |
|
除了使用 LocalDate 提供的便捷的方法用于获取日期中各个字段的值,其实我们还可以通过更通用的 get() 方法来获取年月日这些字段:
1 2 3 4 |
|
get() 方法接收 TemporalField 类型的参数,TemporalField 是一个接口,定义了如何访问某个时间对象中的字段。ChronoField 枚举类实现了此接口,所以我们只需要通过传入相应的枚举就能清晰的表达出取值的意图。
注意,由于 LocalDate 只包含年月日字段,如果我们传入的枚举值是时分秒的话就会抛出异常:
1
|
|
LocalTime 对象只能用于表示时间,即时、分、秒,无法表示年、月、日,并且也不保存任何时区相关的信息。创建 LocalTime 的实例和 LocalDate 类似,也是通过调用静态工厂方法:
1 2 3 4 5 6 |
|
上面代码中前 3 个方法传入的值依次为时、分、秒、纳秒;第 4 个方法用于计算一天中过去多少秒后的时间,如新的一天在过去 36000 秒后的时间是 10:00;第 5 个方法是用于计算一天中过去多少纳秒后的时间,如新的一天在过去 1000000000 秒后的时间是 00:00:01;第 6 个方法通过系统时钟来获取当前的时间,使用起来很便捷。
LocalTime 也提供了用于获取时间对象中各个字段的方法,示例如下:
1 2 3 4 5 |
|
LocalTime 也可以通过 get() 方法来获取各个字段,但注意的是传入的 ChronoField 枚举值不是时、分、秒、纳秒的话也会抛出异常。
LocalDate 和 LocalTime 都支持通过字符串解析来创建相应的对象:
1 2 |
|
其中 parse() 方法还支持传入 DateTimeFormatter 对象,用于指定解析日期的格式,JDK 默认为我们提供了很过的格式,具体用法查询下文档即可,此处不在赘述。
LocalDateTime 是将 LocalDate 和 LocalTime 组合起来,能表示年月日时分秒纳秒,但仍然不能保存时区信息。所有以 Local 开头的时间类都无法保存时区信息,否则 Local 不是会显的很多余吗?LocalDateTime 的性质和上面 2 个类一样都是不可变的,任何操作都会创建新的对象
创建 LocalDateTime 的方法也有很多,很灵活:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
比较有意思的是后面 2 个方法,LocalDate 设置 time 会返回 LocalDateTime,LocalTime 设置 date 也会返回 LocalDateTime。LocalDateTime 也提供了各种方法用于获取各个时间字段的值,同样也支持通用的 get() 方法,传入 ChronoField 枚举来访问,不再赘述。
Instant 类使用一个大整数用于表示自 Unix 元年(1970年1月1日)过去的秒数,不适合人类阅读,它存在的意义就是给机器使用。其精度能达到纳秒级别,通过静态方法 ofEpochSecond() 来创建实例对象,也可以通过静态方法 now() 来获取当前的时间戳:
1 2 |
|
由于 Instant 对象中只存储了秒和纳秒信息,所以我们是无法获取其他时间字段的值,比如下面代码想要通过 Instant 对象获取小时数就会抛异常:
1 2 |
|
到目前为止,上面讲的所有的类都实现了 Temporal 接口,表示的是某个时间点,而 Duration 类用来表示一段时间,即两个时间点间的差。可以通过调用 Duration.between() 静态方法来计算两个时间点的差,但需要注意的是这个方法只能用来计算 LocalTime,LocalDateTime 和 Instant 间的差,不能用与计算两个 LocalDate 的差,因为 Duration 也是由秒和纳秒组成的,而 LocalDate 无法精确到秒所以不能参与计算。此外,由于 LocalDateTime 和 Instant 都能精确到纳秒,但它们的用途不同,前者用于方便人类阅读,而后者使用大整数便于机器计算,所以不能将二者混用,否则会抛异常。
1 2 3 |
|
Duration 类只能用日、时、分、秒或纳秒来表示时间间隔,如果我们想要用年、月、日来表示时间间隔的话,那就需要 Period 类了。下面演示如何计算两个日期的差:
1 2 |
|
Duration 和 Period 类除了通过调用 between() 方法来创建实例外,它们也提供了 of() 静态工厂方法来直接创建实例而不用额外的去创建 2 个时间对象,如下所示:
1 2 3 4 5 |
|
计算、解析、格式化日期
上一节只是讲解了如何创建日期对象,但在开发过程中可没这么简单,我们更多的时候需要对它们进行某些计算,比如计算当前日期 2 天后是星期几等这类问题,所以下面我们将讲解如何根据日期对象来进行计算。
操纵一个日期对象最容易也是最直接的方法就是调用 withAttribute() 方法了,此方法会返回一个全新的对象,而不会在原来对象的基础上进行修改,请看下面的代码:
1 2 3 4 |
|
从上面的代码中可以看出,我们通过调用 withXXX 方法就可以单独设置日期对象中的某些字段了,而最后一个 with 方法属于一个更通用的方法,我们只需传入 ChronoField 枚举即可,但是如果操作的日期对象不包含该枚举字段的话将会抛出异常。
此外,我们还可以使用声明式的编程风格来操作日期,以 LocalDate 为例,请看下面的代码:
1 2 3 4 |
|
从上面的代码中可以看出,我们不再调用 with 方法了,而是使用更具有语义的方法就能修改对象的某个字段,最后的方法也是一个更通用的方法,传入想要修改的字段及数据即可。
下面出个考题考考大家,请运用刚才讲的知识来实现下面的需求:
假设小明的生日是 1990 年 1 月 7 日,那么他 27 岁的生日那天是星期几?
1 2 |
|
是不是很简单。有时我们需要进行更高级的日期操作,比如将某个日期调整到某个月最后一天、或下一个工作日等,这就需要进行某些逻辑判断,而非简单的进行加减,所以新的日期 API 增加了 TemporalAdjuster 用于帮我们进行复制的日期操作,其中 JDK 内置了很多的 adjuster,如果没有符合我们需要的,我们还可以实现 TemporalAdjuster 接口以满足业务需要,这里不再详解了,大家可以参考网上的资料学习,都很简单。
解析和格式化日期是对日期操作的另一个重要组成部分,java.time.format 包下的类就用于此目的。其中最重要的类就是 DateTimeFormatter 了,可以通过此类的静态方法来创建对象。下面演示下如何格式化和解析日期对象:
1 2 3 |
|
从上面的代码中可以看出,我们只要在需要进行格式化的日期对象上调用 format() 方法,传入要格式化的模式即可,而解析也很简单,调用类的静态方法 parse(),传入一个日期格式的字符串,以及解析模式就能还原出日期对象。其中 DateTimeFormatter 内置了很多种日期模式,假如没有我们需要的,我们可以自定义解析格式,请看下面代码:
1 2 3 4 5 6 |
|
上面的代码中,我们定义了一个 DateTimeFormatter 对象,调用 ofPattern 方法为其指定了解析模式为“yyyy/MM/dd”,然后我们用此模式格式化和解析日期,最终都能正确的结果。其中 ofPattern 还有一个重载的方法用于支持本地化,如:
1 2 3 |
|
在 ofPattern 方法中,我们还传入了第二个参数 Locale.CHINA,当我们再格式化日期是,就能看到原来的月份变成了“十二月”。
至此,Java 8 中增加的对日期的新特性就基本讲完了,总之新的 api 给开发者着实带来了开发效率上的提高,从难以使用到现在的傻瓜化,Java 8 确实是 Java 发展中的一个重要里程碑,其影响力不亚于当年 Java 5 发布时泛型、注解给程序员带来的冲击,而下一讲中我将介绍 Java 8 最重要的特性—— Lambda 表达式,它的到来将 Java 领入了函数式编程的世界,开启了 Java 世界的新篇章。