前言
这个标题的文章也是有很多的了,不过我想从我个人的理解去描述一下Java8的时间API,本文将从与老时间API Date类的使用做对比的方式来展开,同时解读一下个人对于Java8的时间API主要接口在代码设计上的理解,欢迎大家讨论与指正
新老API的对比
以前我们在开发中,比如简单的就像获取一个今天的日期,也就是yyyy-MM-dd这种,比如今天,我就想得到一个2019-11-11的字符串,可能我们要这么获取
// 获取今天的Date对象
Date now = new Date();
// 获取时间格式化的对象
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
// 然后进行格式化
String nowStr = dateFormat.format(now);
那Java8的新时间API呢
// 获取LocalDateTime的实例
LocalDate now = LocalDate.now();
// 直接toString即可
String nowStr = now.toString();
虽然有点取巧(LocalDate的默认格式就是yyyy-MM-dd),但是从Java8的API设计的表意来看,语义是更加清楚的,now()方法就是获取当前的日期
上面的例子可能还没有完全展示Java8 API的语义化,我们稍加变化一下要求就可以立马看出来
把获取今天日期的yyyy-MM-dd格式的字符串改为获取昨天日期的yyyy-MM-dd格式的字符串
那老API怎么做呢(可能让人头大)
// 获取Calendar类实例
Calendar calendar = Calendar.getInstance();
// 我都不想写注释了,这个方法太别扭了
calendar.add(Calendar.DATE, -1);
Date yesterday = calendar.getTime();
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
String yesterdayStr = dateFormat.format(yesterday);
大家可以看到,calendar.add(Calendar.DATE, -1)这个add方法真的很别扭,既然叫add了,但是又是加的-1,虽然确实在数学上没毛病,但是这里可能我们在第一次使用中,可能想到了会是找一个叫减的方法,而不会想到是加的方法
我们再来看看Java8的新API怎么做的
// 获取当天时间
LocalDate now = LocalDate.now();
// 减去一天
LocalDate yesterday = now.minusDays(1l);
String yesterdayStr = yesterday.toString();
这个一对比简直吓死人。。。显然Java8的API语法更符合我们日常的思考的方式,它简化了我们去处理复杂时间的内部逻辑,转而用更语义化的API来帮助我们达成我们想要的效果
(这也是我们可以思考的一点,设计API时,让调用者更加关注他们应该关注的问题,我们应该更加抽象与封装,更加内聚,只暴露有用的API参数)
上面的例子还不够酸爽的话,那再来一个,
这次的问题是,判断一下今天是星期几(打出中文的星期一,星期二等)
先看看老API的表现吧
// 获取一个Calendar实例
Calendar calendar = Calendar.getInstance();
// 直接用一个常量Calendar.DAY_OF_WEEK获取
int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
看起来Calendar.DAY_OF_WEEK语义化是很明显,一周的第几天,不过。。。这个竟然是一个数字常量!!!(我开始还以为是一个枚举)
再看看calendar.get()方法,由于参数是一个数字。。。所以你得限制数字的范围啊,所以可以看到类源码里是这样的
真是把我笑到了。。。。。。这个设计算是很屎...
当然你以为这就结束了,太天真了,我之前的要求不是要显示是星期几么,现在calendar.get(Calendar.DAY_OF_WEEK)也是返回了一个数字啊,现在是2019-11-11星期一,所以想着给一个星期几的数组,然后
String[] weekDays = {"星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"};
String weekDay = weekDays[calendar.get(Calendar.DAY_OF_WEEK)-1]
结果一打印,竟然是星期二!!!
emmm...好吧,有误差。。。结果仔细一看Calendar.DAY_OF_WEEK这个注释
人家是从星期日开始算的。。。这个也太差异化了吧,我想要是这个API是中国人写的,敢打赌不会是从星期日开始的。。。
查看了日期的国际标准ISO 8601 清楚写着周一是第一天
不扯了这个了,把之前的数组星期日放在最前面就可以了
String[] weekDays = {"星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"}
从上面的例子也可以看出来,像星期这种,虽然本地化显示有差异,星期一或者monday,但是星期这个概念应该是可以统一的,不需要我们这么去做
那我们再看看Java8 时间API的处理方式
// 获取今天的日期
LocalDate now = LocalDate.now();
// 从中获取它是一周的第几天
DayOfWeek dayOfWeek = now.getDayOfWeek()
可以看到这里返回一周第几天是返回的DayOfWeek,这个进源码一看
我的天,不但是枚举,而且也是从星期一开始的第一天的,我们再使用它的getDisplayName()做本地化的展示
String dayOfWeekStr = dayOfWeek.getDisplayName(TextStyle.FULL, Locale.getDefault());
最后展示的就是星期一
这几个例子显然很能说明问题了,当然Java8的时间API不止以上的功能,它相当于重新把时间这个定义进行拆分,以前一个Date能表示所有的时间,它能表示日期,能表示时间,能表示日期时间,还能表示时区,是一个大而全的时间类,但是这个大而全的感觉就像大而全的单体应用一样,大而全只会对扩展,更新带来阻碍,维护性也更差,所以最终只有进行拆分,Date拆分出了Java8的时间API,大而全的单体应用拆分出分布式,拆分出微服务也是一个道理
Java8时间类介绍
其实上面的也不是很全,但是大家平常常用的基本都罗列到了,而且随便点开一个类,看看里面的方法都是很语义化的,这是LocalDateTime的API
除了每个时间类都有语义化的API之外,还有一个工具类,值得注意,就是每个类都有很多with开头的方法
其实with方法就是调整的意思,也就是你可以随心所欲的去调整你的时间类,尤其是参数为TemporalAdjuster的方法,这个TemporalAdjuster其实就是一个接口,准确来说是一个函数式接口,可以理解为一个时间调整方法,虽然接口定义有点抽象,但是你不用想怎么去实现这个接口,有一个工具类TemporalAdjusters已经帮你完成了很多TemporalAdjuster的实现了(额外补充一点,这种直接在接口后加一个s的方式,也是一种设计API时的方式,表示工具类,比如下面会提到的TemporalQuery和TemporalQueries,再比如线程池Executor和Executors等,所以我们设计API也可以这么参考)
也是很语义化的静态工具方法
我们来简单举个栗子
比如你在代码中拿到一个时间LocalDate,现在是yyyy-MM-dd的形式,需要进行处理
假如你要取到这一天的前一天
// 最简单减一天
LocalDate yesterday = localDate.minusDays(1l);
假如你要取到这一天月和日(MM-dd)
MonthDay monthDay = MonthDay.from(yesterday);
假如你要取到这一年的5月的这一日(直接用with方法,表示修改和调整)
LocalDate localDateWithMay = localDate.withMonth(Month.MAY.getValue());
假如你要获取这一天是那一年的第几天
int dayOfYear = localDate.getDayOfYear();
假如你要获取这一天所在月的第一天
LocalDate firstDayOfMonth = localDate.with(TemporalAdjusters.firstDayOfMonth());
假如你要获取这一天之后的下个星期一
LocalDate nextMonday = localDate.with(TemporalAdjusters.next(DayOfWeek.MONDAY));
当然除了这么好用的API,最关键的Java8的时间类都是final的,也就是绝对的线程安全的,不像Date
Java8时间接口
也写了几年代码了,以前也写了很多业务代码,但是我觉得写代码是很有意思的,不过老写业务代码,只会觉得枯燥,直到我发现,代码有意思的地方其实应该是接口
能用抽象的接口来描述你理解到的业务,我想这个过程才是代码的魅力吧
(所以虽然还是写业务代码,但是我会在可以用接口的地方,用接口来表达我理解到的业务,去在意代码摆放的位置以及命名,不仅仅为了完成功能,就像在做一件艺术品)
所以与其说Java8时间框架,不如说是一组接口,让我们从接口来看看作者眼中的时间是什么样的
随便打开LocalDate,或者LocalDateTime,从它们实现接口层层去找,我们找到了最顶层的接口TemporalAccessor
TemporalAccessor
什么是TemporalAccessor?
Temporal表示是:时间的
Accessor表示是:访问方法,访问器
连在一起也就是时间的访问方法,说起时间,这个词的范围太广太大了,什么各种日历系统,什么夏令时,什么时区,很是复杂,因此从这个接口简单地都可以看得出,设计API的作者是希望我们能够用比较简单的方式来访问时间,降低时间概念本身的复杂性,那怎么简单法呢?让我们来具体看看TemporalAccessor接口的方法吧
boolean isSupported(TemporalField field)
long getLong(TemporalField field)
default ValueRange range(TemporalField field)
default int get(TemporalField field)
default R query(TemporalQuery query)
一共就5个方法,其中只有2个是抽象方法,还有3个default方法,就2个抽象方法,这已经抽象的很简单了,大家注意看这5个方法中用的最多的就是另一个接口TemporalField:时间字段
毕竟TemporalAccessor大量使用了TemporalField,所以要明白TemporalAccessor怎么表示时间,可能需要先明白什么是TemporalField
TemporalField
那什么是时间字段,时间字段本身其实就在描述时间(没有特殊说明的情况下,以下举例的所有时间都按照ISO 8601日历系统来举例的哈),比如就是今天这一刻2019-11-23 14:00:00,你用一天24小时的方式来看它,你可以说时间是14点,你用一周7天,你可以说是星期六,你用一年12月来看,你可以说时间是11月份
简单来说,当你想要和别人描述一个时间时,你或多或少都逃不开一个时间字段,你用不同的时间字段来描述时间,但时间字段本身就是在一种约定的时间范围(一种参考标准)内,当然如果只有一个数字的时间都是让人摸不着头脑的,因为如果从时间本质来看,它是没有数字这一概念的,它就是一条从以前到未来的时间线,我们怎么去描述它,取决于我们怎么去界定这个时间字段,去定义一个参考标准,也因此也才会出现不同的日历系统
就像那句话说得,一千人心中有一千个哈姆雷特!大家每个人的标准不一样,自然看同一个事物就会有不同的结论,同样,同样的一个时间点,不同时间字段也就是时间标准去看也是不同
所以Java8时间API想要给帮我们简化出来的“时间”应该即: 用数字+时间字段的方式来描绘时间
是不是如此,让我们来看看这个时间字段TemporalField接口到底是什么样的,都有哪些主要方法吧
default String getDisplayName(Locale locale)
TemporalUnit getBaseUnit()
TemporalUnit getRangeUnit()
ValueRange range()
boolean isDateBased()
boolean isTimeBased()
晃眼一看,方法貌似很多,不过我们一个方法一个方法的来
default String getDisplayName(Locale locale)
这个方法好理解吧,获取这个时间字段的展示名字,很需要,因为比如周一,中文叫星期一,但是用英文叫Monday,所以传参是本地化
TemporalUnit getBaseUnit()
获取这个时间字段的基本单位,这个也好理解,比如之前那个例子,说时间是14点,是站在一天有24小时的基础上说的,所以基本单位肯定是小时,但是从一周7天来看,是周六,基本单位应该是天,不同维度的时间字段肯定会有一个基本度量单位
TemporalUnit getRangeUnit()
这个方法也好理解,获取这个时间字段的范围的单位。继续之前的例子,说时间14点,基本单位咱们是小时,小时是站在一天的角度说的,所以范围的单位就是天了,同理说时间是12月,基本单位肯定格式月份,月份是站在一年12月角度说的,所以范围的单位就是年了
ValueRange range()
获取这个时间字段的范围,返回的ValueRange虽然不认识,不过从名字也可以看出来是范围的抽象,你可以简单的理解为最大和最小值。不过其实它是有4个字段的
依次是最小的最小值,最小的最大值,最大的最小值,最大的最大值
,这样其实最大值和最小值某种意义说是不精确的,只是一个最小值和最大值的范围,当然我们举个例子就明白了
例如一天24小时这个时间字段,那范围肯定就是0-23,最小的最小值和最小的最大值都是0,最大的最小值和最大的最大值也都是23
但是如果是一个月中的天数这样的时间字段,不就不一样了嘛,最小的最小值和最小的最大值肯定都是1,但是最大的最小值应该是28天,最大的最大值是31了
boolean isDateBased()
boolean isTimeBased()
两个方法一起看,分别代表是否支持日期(年月日),是否支持时间(时分秒),比如一天24小时这种时间字段,显然是不支持日期的,而星期这个时间字段,显然是不支持时间的
了解了TemporalField那现在回过头再看看我们的接口TemporalAccessor方法,是不是就好理解点了,这个接口就是暴露给我们这个“时间”拥有的信息,TemporalAccessor接口是在描述这个“时间”是什么样子的
// 这个时间是否支持这个时间字段(比如若只是2019-11-21,你不能去描述说这个时间小时是多少)
boolean isSupported(TemporalField field)
// 这个时间在这个时间字段的值是多少(处理小值,比如一年有几个月)
long getLong(TemporalField field)
// 这个时间在这个时间字段的范围是多少(比如一周是1-7天,一天是24小时)
default ValueRange range(TemporalField field)
// 这个时间在这个时间字段的值是多少(处理大值,比如一年有多少毫秒)
default int get(TemporalField field)
// 这个时间里的某个信息是多少
default R query(TemporalQuery query)
前面4个方法都还是比较好理解的,但最后一个,参数是TemporalQuery,但是返回是一个泛型R,R是什么类型?其实可以这么想,既然泛型R没有限制,那我们是不是可以简单理解为任何和时间相关的信息都可以用这个方法查到,比如该时间用的是哪个日历系统(不同的日历系统有不同的时间字段定义),这个时间的精度是多少等
TemporalQuery其实就是一个函数式接口,里面只有一个表示怎么获取时间信息的方法queryFrom
@FunctionalInterface
public interface TemporalQuery {
R queryFrom(TemporalAccessor temporal);
}
结合以上,我们可以看到TemporalAccessor接口完全就是一个只读的接口,用于各种维度的去描述时间,但是可以知道的是,其实我们日常还是会有很多对于时间做运算的场景,比如知道今天的日期,算明天的日期,知道今天的日期,算1年2个月后的日期等,所以只有只读的接口抽象显然也是很不完整的
Temporal
所以接下来我们就会看到TemporalAccessor的子类接口Temporal
Temporal就是一个对于“时间”进行写的接口,由于继承了TemporalAccessor,因此Temporal的子类就可以表示是读写操作的“时间”(题外话,Temporal和TemporalAccessor这感觉很像docker的镜像和容器,镜像就是只读层,而容器就是在镜像的基础上加一层可读可写层),让我们来看看Temporal都有哪些方法吧
default Temporal with(TemporalAdjuster adjuster)
Temporal with(TemporalField field, long newValue)
default Temporal plus(TemporalAmount amount)
Temporal plus(long amountToAdd, TemporalUnit unit)
default Temporal minus(TemporalAmount amount)
default Temporal minus(long amountToSubtract, TemporalUnit unit)
long until(Temporal endExclusive, TemporalUnit unit)
这里的plus,minus其实都还是好理解,就是加减嘛,只是会涉及到两个新接口TemporalUnit和TemporalAmount,其实直译就是时间单位和时间数量,而且从接口来看,每一个TemporalAmount相关的plus,minus方法,就会有一个同名的TemporalUnit相关的接口,那我们是不是可以简单的理解他们之间应该以下关系
一组或多组(数量+时间单位) = 时间数量
那我们来看看TemporalUnit和TemporalAmount接口
TemporalUnit
TemporalUnit之前TemporalField有提到,代表的是时间单位
它有以下主要方法
Duration getDuration()
第一个方法最重要,返回Duration,Duration代表一个持续的时间,可以说是时间的数量,毕竟单位其实就是一个数量基准,就像地图比例尺,以200米作为基准,以1公里作为基准,时间这里也类似。
一小时按照秒单位来看就是3600秒,按照分钟单位来看,就是60分钟
TemporalAmount
再来看看TemporalAmount接口的主要方法
long get(TemporalUnit unit)
List getUnits()
很表意的方法,第一方法代表这个时间数量里这个时间单位的值是多少,第二个方法代表当前这个时间数量有哪些时间单位,比如3年2个月,这个就是一个时间数量,2个小时,这也是一个时间数量
所以总的来说:一组或多组(数量+时间单位) = 时间数量
这么理解了TemporalUnit和TemporalAmount两个接口,那它们与Temporal相关的4个方法也好理解了,无非就是根据时间的量去做加减法
至于Temporal剩下的方法
default Temporal with(TemporalAdjuster adjuster)
Temporal with(TemporalField field, long newValue)
with方法之前说过就是代表调整,修改,所以第二个方法也很简单,根据某个时间字段来修改,比如2019-11-23按照一年12个月,把月修改为1月,得到2019-01-23,这不是加减法,而是直接替代某个值
还有最后一个方法
long until(Temporal endExclusive, TemporalUnit unit)
之前不是说时间不就是一条线么,所以这个方法理解起来就容易了,也就是这条线上两个时间点,相聚多少个时间单位
其实到此为止,我们沿着TemporalAccessor和Temporal,把时间API的基本抽象描述的差不多了,因为我们看看TemporalAccessor和Temporal所在文件夹就知道了
涉及到的接口都有讲到了,基于这些接口的理解,你再去看时间API相关的实现类,或者用一些实现类的方法时就不会再摸不着头脑,因为只要不懂的类,往上看到接口,基本都是上面几个类,你就会明白这个实现类在时间API里占据的什么角色,然后再看实现类,就会很容易理解了
最后简单画个沿着TemporalAccessor和Temporal的关系涉及到的相关接口的图