译者前言:翻译的是一篇两年前的文章,起因是Stack Overflow上有很多关于java.util.Date
的问题,当然其中不少是小白问的,不过也间接的说明这个类是比较坑的,不然不会出这么多问题,包括java.sql.Date
的出现,还有它类上的注释,以及下文提到的20年前java.util.Date
的大部分方法就被废弃了,都佐证了这一点,这个类的设计是失败的。
原文是英国大神Jon Skeet写的,十四年前就写开始技术博客了,OOP鼻祖,是一位很强的大神,stackoverflow社区说“他的代码如果不能运行,那一定是编译器的错”。下面正文:
很少有像
java.util.Date
的java类在Stack Overflow上引起如此多的问题。这有四个原因:
- 日期和时间底层实现相当复杂,同时会遇上各种情况。虽然它是可管理的,但需要花时间去理解它。
java.util.Date
在很多方面都很糟糕(详见下文)。- 一般来说,开发人员对它了解很少。
- 它被很多类库严重滥用,进一步加剧了混乱。
简而言之
- 如果可能的话,你应该避免它。尽可能使用
java.time.*
,如果您还没有使用Java 8 及以上版本,则使用ThreeTen-Backport(基本上是旧版本的java.time)或Joda Time。- 如果你非要用它,就避免使用已废弃的方法。他们中的大多数已被弃用近20年,并且有充分的理由。
- 如果您真的觉得必须使用已弃用的方法,请确保真正理解这些方法。
一个
Date
实例表示instant的时间,而不是日期。这意味着Date
存在以下问题:
- 没有时区。
- 没有格式。
- 没有日历系统。
现在一条一条的喷java.util.Date
java.util.Date
(下文简称Date
)是一个糟糕的类型,这解释了为什么它在Java 1.1中被废弃了很多内容(但不幸的是,它仍被使用)。设计缺陷包括:
- 它的名字具有误导性:它不代表一个
Date
,它代表的是“即时”。所以它的类名其实应该写成Instant
,因为它和java.time
是等价物。- 它不是常量:鼓励继承类更改时间,例如
java.sql.Date
(意思是代表一个日期,也因为有相同的简称而混淆)- 它是可变的:日期/时间类型是自然值,它们由不可变类型有用地建模。
Date
可变的事实(例如通过该setTime
方法)意味着勤奋的开发人员最终会在整个地方创建防御性副本。- 它隐含地在许多地方使用系统本地时区 - 包括
toString()
- 这使许多开发人员感到困惑。- 它的月份编号从0开始,是从C复制过来的。这导致了很多很多的错误。
- 它的年份编号是1900年,也是从C复制的。当Java出来的时候我就有一个想法,这对可读性有害吗?
- 其方法名称不明确:
getDate()
返回日期,getDay()
返回星期几。给那些更具描述性的名字有多难?- 关于它是否支持闰秒是不明确的:“第二个由0到61之间的整数表示; 值60和61仅在闰秒发生,甚至仅在实际正确跟踪闰秒的Java实现中发生。“我强烈怀疑大多数开发人员(包括我自己)做出了大量假设,其范围
getSeconds()
实际上在0范围内-59包容。- 设置的时间不必在合法的范围内; 例如,日期可以指定为1月32日,然后被解释为2月1日,这不是在搞笑吗?
我可以找到更多的问题,但他们会变得更挑剔。这是一个很好的清单。从积极的一面:
- 它明确地表示单个值:instant,没有关联的日历系统,时区或文本格式,精确到毫秒。
不幸的是,开发人员对这个“好方面”的了解甚少。我们细说......
什么是“instant”?
注意:我相对性的忽略了本文其余部分和闰秒。它们对某些人来说非常重要,但对于大多数读者来说,会搞得更多人看不懂。
当我谈到“即时”时,我正在谈论可用于识别事情何时发生的那种概念。(可能是将来,但最容易考虑过去的情况。)它与时区和日历系统无关,因此多人使用他们的“本地”时间表示可以用不同的方式来讨论它。
让我们使用一个非常具体的例子,说明在某个地方不会使用我们熟悉的任何时区:Neil Armstrong在月球上行走。月球漫步在特定时刻开始,如果来自世界各地的多个人同时观看,他们都会(几乎)同时说“我现在可以看到它”。
如果你在休斯敦的任务控制中观察,你想的那个即时是“1969年7月20日,CDT时间9:56:20”。如果你在伦敦观看,你想的那个即时是“1969年7月21日,BST凌晨3:26:20”。如果你在沙特的利雅得观看,你想的那个即时是“Jumādá7th1389,5:56:20 am(+03)”(使用Umm al-Qura日历)。即使不同的观察者会在他们的时钟上看到不同的时间 - 甚至不同的年份 - 他们仍然会考虑同一时刻。他们只是应用不同的时区和日历系统,从即时转变为更加以人为中心的概念。
那么计算机如何表示即时呢?它们通常在特定时刻之前或之后存储一定量的时间,该时刻实际上是原点。许多系统都使用Unix时代,这是UTC中格里高利历中表示的即时,即1970年1月1日午夜。这并不意味着时代本身就是“在”UTC中 - 同样可以定义Unix时代作为“1969年12月31日晚上7点在纽约的时刻”。
该
Date
班采用“毫秒自Unix纪元” -这是返回的值getTime()
,以及在由设置Date(long)
构造函数或setTime()
方法。由于月球行走发生在Unix时代之前,因此价值为负:它实际上是-14159020000。为了演示如何
Date
与系统时区进行交互,让我们展示之前提到的三个时区 - 休斯顿(美国/芝加哥),伦敦(欧洲/伦敦)和利雅得(亚洲/利雅得)。当我们从其划分时间值构建日期时,系统时区是什么并不重要 - 这完全不依赖于当地时区。但是如果我们使用Date.toString()
,它会转换为当前的默认时区以显示结果。更改默认时区根本不会更改该Date
的值。对象的内部状态完全相同。它仍然代表同一时刻,但方法一样toString()
,getMonth()
并getDate()
会受到影响。以下示例代码显示:
import java.util.Date;
import java.util.TimeZone;
public class Test {
public static void main(String[] args) {
// The default time zone makes no difference when constructing
// a Date from a milliseconds-since-Unix-epoch value
Date date = new Date(-14159020000L);
// Display the instant in three different time zones
TimeZone.setDefault(TimeZone.getTimeZone("America/Chicago"));
System.out.println(date);
TimeZone.setDefault(TimeZone.getTimeZone("Europe/London"));
System.out.println(date);
TimeZone.setDefault(TimeZone.getTimeZone("Asia/Riyadh"));
System.out.println(date);
// Prove that the instant hasn't changed...
System.out.println(date.getTime());
}
}
输出如下:
Sun Jul 20 21:56:20 CDT 1969
Mon Jul 21 03:56:20 GMT 1969
Mon Jul 21 05:56:20 AST 1969
-14159020000
输出中的“GMT”和“AST”缩写是非常不幸的 -
java.util.TimeZone
在所有情况下都没有1970年以前的正确名称。虽然本次是正确的。常见问题
如何将
Date
转换为其他时区?转不了 - 因为
Date
没有时区。这是一瞬间的时间。不要输出愚蠢的toString()
。那是在默认时区显示的时间。它没有价值。
如果以Date
作为输入,则已经发生从“本地时间和时区”到即时的任何转换。(希望它能正确完成...)如果你打算编写一个下面这样的方法,你只能掉坑里:
// 不管怎么写都是错的
Date convertTimeZone(Date input, TimeZone fromZone, TimeZone toZone)
如何将
Date
转换为其他格式?转不了,因为
Date
没有格式。不要输出愚蠢的toString()
。它总是使用相同的格式,如文档所述。 要Date
以特定方式格式化,请使用合适的DateFormat
(一般用SimpleDateFormat
) - 记住将时区设置成你要用的时区。
原文:https://codeblog.jonskeet.uk/2017/04/23/all-about-java-util-date/