转载

Java8 时间API及主要接口个人理解

前言

这个标题的文章也是有很多的了,不过我想从我个人的理解去描述一下 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 ),但是从 Java8API 设计的表意来看,语义是更加清楚的, 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();

这个一对比简直吓死人。。。显然 Java8API 语法更符合我们日常的思考的方式,它简化了我们去处理复杂时间的内部逻辑,转而用更语义化的 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 语义化是很明显,一周的第几天,不过。。。这个竟然是一个数字常量!!!(我开始还以为是一个枚举)

Java8 时间API及主要接口个人理解

再看看 calendar.get() 方法,由于参数是一个数字。。。所以你得限制数字的范围啊,所以可以看到类源码里是这样的

Java8 时间API及主要接口个人理解

Java8 时间API及主要接口个人理解

Java8 时间API及主要接口个人理解

真是把我笑到了。。。。。。这个设计算是很屎...

当然你以为这就结束了,太天真了,我之前的要求不是要显示是星期几么,现在 calendar.get(Calendar.DAY_OF_WEEK) 也是返回了一个数字啊,现在是 2019-11-11 星期一,所以想着给一个星期几的数组,然后

String[] weekDays = {"星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"};
    String weekDay = weekDays[calendar.get(Calendar.DAY_OF_WEEK)-1]

结果一打印,竟然是 星期二 !!!

Java8 时间API及主要接口个人理解

emmm...好吧,有误差。。。结果仔细一看 Calendar.DAY_OF_WEEK 这个注释

Java8 时间API及主要接口个人理解

人家是从星期日开始算的。。。这个也太差异化了吧,我想要是这个 API 是中国人写的,敢打赌不会是从星期日开始的。。。

查看了日期的国际标准 ISO 8601 清楚写着周一是第一天

Java8 时间API及主要接口个人理解

不扯了这个了,把之前的数组星期日放在最前面就可以了

String[] weekDays = {"星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"}

从上面的例子也可以看出来,像星期这种,虽然本地化显示有差异,星期一或者monday,但是星期这个概念应该是可以统一的,不需要我们这么去做

那我们再看看 Java8 时间 API 的处理方式

// 获取今天的日期
    LocalDate now = LocalDate.now();
    // 从中获取它是一周的第几天
    DayOfWeek dayOfWeek = now.getDayOfWeek()

可以看到这里返回一周第几天是返回的 DayOfWeek ,这个进源码一看

Java8 时间API及主要接口个人理解

我的天,不但是枚举,而且也是从星期一开始的第一天的,我们再使用它的 getDisplayName() 做本地化的展示

String dayOfWeekStr = dayOfWeek.getDisplayName(TextStyle.FULL, Locale.getDefault());

最后展示的就是星期一

这几个例子显然很能说明问题了,当然 Java8 的时间 API 不止以上的功能,它相当于重新把 时间 这个定义进行拆分,以前一个 Date 能表示所有的时间,它能表示日期,能表示时间,能表示日期时间,还能表示时区,是一个大而全的时间类,但是这个大而全的感觉就像大而全的单体应用一样,大而全只会对扩展,更新带来阻碍,维护性也更差,所以最终只有进行拆分, Date 拆分出了 Java8 的时间 API ,大而全的单体应用拆分出分布式,拆分出微服务也是一个道理

Java8时间类介绍

Java8 时间API及主要接口个人理解

其实上面的也不是很全,但是大家平常常用的基本都罗列到了,而且随便点开一个类,看看里面的方法都是很语义化的,这是 LocalDateTimeAPI
Java8 时间API及主要接口个人理解

除了每个时间类都有语义化的 API 之外,还有一个工具类,值得注意,就是每个类都有很多 with 开头的方法

Java8 时间API及主要接口个人理解

Java8 时间API及主要接口个人理解

其实 with 方法就是调整的意思,也就是你可以随心所欲的去调整你的时间类,尤其是参数为 TemporalAdjuster 的方法,这个 TemporalAdjuster 其实就是一个接口,准确来说是一个函数式接口,可以理解为一个时间调整方法,虽然接口定义有点抽象,但是你不用想怎么去实现这个接口,有一个工具类 TemporalAdjusters 已经帮你完成了很多 TemporalAdjuster 的实现了(额外补充一点,这种直接在接口后加一个 s 的方式,也是一种设计 API 时的方式,表示工具类,比如下面会提到的 TemporalQueryTemporalQueries ,再比如线程池 ExecutorExecutors 等,所以我们设计 API 也可以这么参考)

Java8 时间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

Temporal 表示是:时间的

Accessor 表示是:访问方法,访问器

连在一起也就是时间的访问方法,说起时间,这个词的范围太广太大了,什么各种日历系统,什么夏令时,什么时区,很是复杂,因此从这个接口简单地都可以看得出,设计 API 的作者是希望我们能够用比较简单的方式来访问时间,降低时间概念本身的复杂性,那怎么简单法呢?让我们来具体看看 TemporalAccessor 接口的方法吧

boolean isSupported(TemporalField field)
    
    long getLong(TemporalField field)
    
    default ValueRange range(TemporalField field)
    
    default int get(TemporalField field)
    
    default <R> R query(TemporalQuery<R> query)

一共就5个方法,其中只有2个是抽象方法,还有3个default方法,就2个抽象方法,这已经抽象的很简单了,大家注意看这5个方法中用的最多的就是另一个接口 TemporalField时间字段

那什么是时间字段,时间字段本身其实就在描述时间,比如就是今天2019-11-23 14:00:00,你用一天24小时的方式来看它,你可以说时间是14点,你用一年365天来看,你可以说时间是第327天

简单来说,当你想要和别人描述一个时间时,你或多或少都逃不开一个时间字段,你用不同的时间字段来描述时间,其实就是在选择一种约定的时间范围去描述时间,当然如果只有一个数字的时间都是让人摸不着头脑的,因为如果从时间本质来看,它是没有数字这一概念的,它就是一条从以前到未来的 时间线 ,我们怎么去描述它,取决于我们怎么去定义这个时间字段,因此也才会出现不同的日历系统

所以从上可以看到, Java8 时间 API 想要给帮我们简化出来的“时间”即: 用数字+时间字段的方式来描绘时间

那现在回过头再看看我们的接口 TemporalAccessor 方法,是不是就好理解点了,这个接口就是暴露给我们这个“时间”拥有的信息, TemporalAccessor 接口是在描述这个“时间”是什么样子的

// 这个时间是否支持这个时间字段(比如若只是2019-11-21,你不能去描述说这个时间小时是多少)
    boolean isSupported(TemporalField field)
    
    // 这个时间在这个时间字段的值是多少(处理小值,比如一年有几个月)
    long getLong(TemporalField field)
    
    // 这个时间在这个时间字段的范围是多少(比如一周是1-7天)
    default ValueRange range(TemporalField field)
    
    // 这个时间在这个时间字段的值是多少(处理大值,比如一年有多少毫秒)
    default int get(TemporalField field)
    
    // 这个时间里的某个信息是多少
    default <R> R query(TemporalQuery<R> query)

前面4个方法都还是比较好理解的,最后一个,其实可以这么想,由于泛型 R 没有限制,我们简单理解为任何和时间相关的信息都可以用这个方法查到,比如该时间用的是哪个日历系统(不同的日历系统有不同的时间字段定义),这个时间的精度是多少等

TemporalQuery 其实就是一个函数式接口,里面只有一个表示怎么获取时间信息的方法 queryFrom

@FunctionalInterface
    public interface TemporalQuery<R> {
        R queryFrom(TemporalAccessor temporal);
    }

结合以上,我们可以看到 TemporalAccessor 接口完全就是一个 只读 的接口,用于各种维度的去描述时间,但是可以知道的是,其实我们日常还是会有很多对于时间做运算的场景,比如知道今天的日期,算明天的日期,知道今天的日期,算1年2个月后的日期等,所以只有只读的接口抽象显然也是很不完整的

所以接下来我们就会看到 TemporalAccessor 的子类接口 Temporal

Temporal 就是一个对于“时间”进行写的接口,由于继承了 TemporalAccessor ,因此 Temporal 的子类就可以表示是 读写 操作的“时间”(题外话, TemporalTemporalAccessor 这感觉很像 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)

这里的 plusminus 其实都还是好理解,就是加减嘛,只是会涉及到一个新接口 TemporalUnitTemporalAmount ,其实直译就是时间单位和时间数量

一个数量+一个时间单位 = 一个时间数量

因此我们可以看到 plusminus 方法都有两个同名方法重写方法,参数分别是 TemporalAmountlongTemporalUnit

那剩下的方法

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`所在文件夹就知道了
![image.png](/img/bVbADtV)

涉及到的接口都有讲到了,基于这些接口的理解,你再去看时间API相关的实现类,或者用一些实现类的方法时就不会再摸不着头脑,因为只要不懂的类,往上看到接口,基本都是上面几个类,你就会明白这个实现类在时间API里占据的什么角色,然后再看实现类,很容易理解
原文  https://segmentfault.com/a/1190000021091213
正文到此结束
Loading...