今天看啥  ›  专栏  ›  jokermonn

闲谈为什么慎用 Date

jokermonn  · 掘金  · android  · 2018-08-06 00:00

前言

作为一个 Java 程序员,尤其是常年使用 Java7 编程的 Android 程序员,如果在开发中涉及到操作日期、时间等需求,大部分小伙伴应该考虑到的都是 java.util.Date 这个类。例如打印当前时间 ——

Date date = new Date();// Date date = new Date(System.currentTimeMillis()); // 与上等价System.out.println(date);

这看起来没啥毛病,因为可能大部分需求仅需要到此就好了。假如现在的需求是要获取当前的年、月、日,以 2018-08-04(星期六) 举例——

Date date = new Date(System.currentTimeMillis());System.out.println(date.getYear() + "-" + date.getMonth() + "-" + date.getDay());

打印结果为:

118-7-6

作为一个程序员应该敏锐地感觉到这个输出值的不友好,从源码或者 Date 注释中不难找到答案——年份的值是当前年份距离1900年在数值上的差值(这一点 Java 参考了 C),月份是从 0 开始计算的(这一点也是参考了 C),而当前日的取值笔者使用的是 getDay(),它的取值是 day-of-week,实际上 Date 还提供了 getDate(),它的取值是 day-of-month。这样令人容易混淆的 api 设计实在是有些糟糕!

不仅如此,Date 这个类给开发者的第一印象应该是日期,也就是记录到年月日的一个类,但是注释里面却提到

The class Date represents a specific instant in time, with millisecond precision.

这意味着 Date 是用来记录一个「瞬间」的而不是仅仅局限于记录年月日,甚至拥有毫秒级别的精度,但是与此同时 Date 类竟然并不是 final 的,这意味着开发者还可以通过 setTime() 去改变「瞬间」。除此之外,开发者甚至可以创建1月32日这样的日期——new Date(2018, 0, 32),打印结果会告诉你它是2月1日—— Fri Feb 01 00:00:00 CST 3918,Java “聪明地”帮开发者修复了 bug。除此之外,即使 Date 作为记录「瞬间」的类,它也仍然是不合格的,根据常识可以知道,衡量一个瞬间除了记录年月日时分秒之外,还应该记录时区,但是 Date 对象中并没有包含时区这个关键属性,而仅仅是在 toString() 方法中 new 了一个时区对象并打印出来,这明显是不符合常理的,举个例子——

在北京的开发者将一篇周报发给了在华盛顿的老总,这篇周报使用了一个 Date 对象来记录周报的创建日期,远在华盛顿的老总清早地没刷牙就打开周报,最后发现底部的日期为 Sat Aug 04 17:35:24 PDT 2018,于是他大发雷霆质问北京的开发者,你的周报创建时间为什么是美国时间8月4日17点35分(PDT 时区),明明现在的美国时间是8月4日5点35分!远在中国的开发者看了看周报的日期,底部毅然写着 Sat Aug 04 17:35:24 CST 2018,这明明是北京时间8月4日17点35分(CST 时区),为什么到了美国就成了美国时间8月4日17点35分了呢?答案就是在于 toString() 方法中的时区是取得当前系统默认的时区,在北京应该就是北京时间8月4日17点35分,在华盛顿就是美国时间8月4日17点35分。所以问题就在于时区这个属性并没有在创建对象的时候就赋值并后期不可改,而是在 toString() 的时候去动态创建,这就造成了上面的误解。

以上令人困惑的设计真是糟糕透顶了!

当然,机智的读者可能早就意识到了,Date 注释当中已经提及到了——使用 java.util.Calendar 类来替代 Date 当中部分过时的方法。是的,早在 JDK1.1 版本的时候,语言的设计者们就意识到了这个问题,并创建了 Calendar 类来弥补 Date 类中的许多方法的缺陷,但是 Calendar 真的解决了问题吗?

Calendar 代表着日历,但是它拥有获取当年时分秒的方法... …笔者认为这一点就足以让 Calendar 尽早 @Deprecated 了;除此之外,Calendar 的月份也是从 0 开始计算的;除此之外如果开发者想要打印相应格式的日期,会发现 JDK 并没有提供这样的类,只能将 Calendar 转换为 Date 再使用 DateFormat 来创建指定格式的日期。

这样的设计同样是糟糕透顶的!

Tiago Fernandez 曾组织过一次最烂 Java api 投票 ——

可以看到日期 api 排到了第二…

替代方案 in Java

before Java8

Java8 以前,建议使用 ThreeTen-Backport 或 Joda Time 来弥补 Java 对时间处理的短板。当然,由于现在 JDK 版本的普及且 Java8 中的新式日期处理就是 Joda Time 遵循 JSR-310 规范重写的版本,所以在这里就不对这两个库做介绍了。

after Java8

针对前文所提到的缺陷,Java8 中提出了一系列位于 java.time 包下新的类用以处理日期相关操作,譬如:LocalDate,LocalTime,LocalDateTime,ZonedDateTime 等。很重要的一点是,这些类都是 immutable and thread-safe 的,并将日期和时间点的处理分配到各个具体的类使其各司其职,不再让开发者们因类名和实际作用不符而感到困惑。

例如 LocalDate 类,顾名思义,此类仅提供简洁的年月日信息,而不含时分秒信息。你可以借助 LocalDate.of(year, month, day)/LocalDate.now() 来创建实例——

LocalDate date = LocalDate.of(2018, 08, 04);LocalDate date = LocalDate.now();

除此之外,LocalDate 还提供了一系列知名晓义的 api——

LocalDate 常用 api

与此对应记录时分秒的类是 LocalTime,它的用法与 LocalDate 相同。当然,为了便于开发者开发,还有一个 LocalDateTime,它实际上就是 LocalDate 和 LocalTime 的合体,在此就不做额外扩展了。

从源码注释可以看出,LocalDate/LocalTime/LocalDateTime 是无关乎时区的,这意味着它们内部不存有关于时区的任何信息,如果想要当前类与时区能够挂上钩,应当使用 ZonedDateTime,其实现就是依靠内部存储了一个 LocalDateTime 对象和一个 ZoneId 时区对象。

所以相比于原有的 Date,这些类做了哪些优化让开发者们会更倾向于使用它们呢?以 LocalDate 为例:

1.final:这意味着该类无法被继承,只需要在本类中控制好暴露的 api,就能保证对象的不可变性。

2.返回值的可读性:

LocalDate date = LocalDate.now();System.out.println(date.getYear() + "-" + date.getMonth() + "-" + date.getDayOfMonth());

2018-AUGUST-5

3.职责分明:LocalDate 负责年月日,LocalTime 负责时分秒,ZonedDateTime 是存储了与时区关联的 LocalDateTime,看这些类的类名就令开发者不会困惑。

4.不再自以为是帮助开发者容错:

LocalDate localDate = LocalDate.of(2018, 1, 32);System.out.println(localDate);

它将会抛出 java.time.DateTimeException,告知开发者取值范围应为 1 - 28/31

除了上述阐述到的这些类以外,java.time 包下还有 Duration,Period,Instant 等类帮助开发者更好地处理日期信息,例如 Instant 就是针对前文所阐述的「瞬间」所创建的类,但是笔者在日常开发中较少接触这些类,故在此就不做额外阐述了。

替代方案 in android

对于 Android 程序员来说,Java8 之后的操作和上述的替代方案 in Java中一样,但是 Java8 并不与上完全一致,因为 Threetenbp /Joda-Time 是通使用 jar 包来加载时区信息的,这个效率在 Android 上实际并不高,所以 JakeWharton 就创建了 ThreeTenABP 库来适配 Threetenbp /Joda-Time 在 Android 上的应用,原理其实很简单,不再使用 jar 包来加载时区信息,而是通过放在 assets 文件夹下的 TZDB.dat 文件来获取时区信息,这样运行在 Android 上的效率将会更高。

注:本文未特别指出的 Date 均为 java.util.Date。




原文地址:访问原文地址
快照地址: 访问文章快照