Java 的日期 API 真烂

date 记得在我刚学 Java 的时候,真是搞不清楚 Date 和 Calendar 这两个类,后来我渐渐知道,原来不能全怪我啊,Java 日期 API 之烂是公认的(不妨参见 这篇文章 ,Tiago Fernandez 做过一个投票,就是要选举最烂的 Java API,结果 Java 日期 API 排行第二,仅次于臭名远扬的 EJB2,嘿嘿)。

蛋疼的 java.sql.Date

只有 Date 和 Calendar 搞定一切吗?那还好啊。当然不是!光 Date 就有 java.util.Date 和 java.sql.Date,而且关系是 java.sql.Date extends java.util.Date。为了把前者转成后者,我写了这样的代码:

Date date = new Date();
java.sql.Date d = new java.sql.Date(date.getTime());

居然不支持 Date 参数的构造器,我只好传入 long 类型的时间。接下去,我尝试把当前小时数取出来:

System.out.println(d.getHours());

悲剧出现了:

Exception in thread "main" java.lang.IllegalArgumentException
	at java.sql.Date.getHours(Date.java:177)

一看源码,坑爹啊:

public int getHours() {
    throw new java.lang.IllegalArgumentException();
}

在 java.util.Date 里面好好的方法怎么变成这个鸟样了?

方法注释给出了说明:

This method is deprecated and should not be used because SQL Date values do not have a time component.

也就是说,java.sql.Date 是 SQL 中的单纯的日期类型,哪会有时分秒啊?我觉得它根本不应该设计成 java.util.Date 的子类。如果你把 java.sql.Date 通过 JDBC 插入数据库,你会发现时分秒都丢失了,因此如果你同时需要日期和时间,你应该使用 Timestamp,它也是 java.util.Date 的子类。

另外还有一个 java.util.Date 的子类叫 Time,java.sql 包下面的 Date、Time 和 Timestamp 可以放在一起记忆。Date 只包含年月日信息、Time 只包含时分秒信息,而 Timestamp 则包含时间戳的完整信息。

现在知道人家抛出 IllegalArgumentException 的用心良苦了吧……

坑爹的 year 和 month

看看 Date 类的构造器:

public Date(int year, int month, int day)

长得并不奇葩嘛。

好,现在我要输出 2012 年的 1 月 1 号了:

Date date = new Date(2012,1,1);
System.out.println(date);

结果,我傻眼了:

Thu Feb 01 00:00:00 CST 3912

等等,这是啥?3192 年?

原来实际年份是要在你的年份参数上加上个起始年份 1900。

更坑爹的是,月份参数我不是给了 1 吗?怎么输出二月(Feb)了?

Date 里面的月份居然是用 0~11 表示的,换句话说,一月用 0 来表示,二月用 1 来表示 。如果不用常量或者枚举,很容易踩到坑里去,对不对?

后来发现 Go 语言的 time.Date 方法,对于月份做了个恶心但是不容易坑人的处理(看奇葩的月份参数啊):

func Date(year int, month Month, day, hour, min, sec, nsec int, loc *Location)

我甚至怀疑 Google 这样处理是在用极端的方法鄙视 Java(另,据我所知,JavaScript 好像也是这样的,月份从 0 开始)……

坑爹的事情还没完,前面已经说了,构造函数的时间起始基准是 1900 年,可是 getTime() 方法却特立独行,返回的时间是相对于“1970-01-01 00:00:00” 的毫秒数差值……

尝试 Joda 吧

最开始的时候,Date 既要承载日期信息,又要做日期之间的转换,还要做不同日期格式的显示,职责较繁杂,从 JDK 1.1 开始,这三项职责分开了:

  1. 使用 Calendar 类实现日期和时间字段之间转换;
  2. 使用 DateFormat 类来格式化和分析日期字符串;
  3. 而 Date 只用来承载日期和时间信息。

原有 Date 中的相应方法已废弃。不过,无论是 Date,还是 Calendar,都用着太不方便了,这是 API 没有设计好的地方。

比如 Calendar 的 getInstance 方法,并未提供一个指定年月日和时分秒的重载方法,每次要指定特定的日期时间, 必须先获取一个表示当前时间的 Calendar 实例,再去设值 ,比如:

Calendar c = Calendar.getInstance();
c.set(2012, 0, 1, 11, 11, 11);
System.out.println(c.getTime());

注意上面代码中对于年份的传值—— 是的,和 Date 不一样的是,Calendar 年份的传值不需要减去 1900(当然月份的定义和 Date 还是一样),这种不一致真是让人抓狂!

打印:

Sun Jan 01 11:11:11 CST 2012

有很多开源库都在努力弥补 Java 的这一问题,比如 Joda-Time,获取 Calendar 对象和设置时间完全可以合成一步完成:

DateTime dateTime = new DateTime(2012, 1, 1, 11, 11, 11, 0);

而且,一月份总是可以传 1 来表示了。

再如,如果要给上述时间增加 3 天再按格式输出的话,使用 Joda 更加便捷:

System.out.println(dateTime.plusDays(3).toString("E MM/dd/yyyy HH:mm:ss");

有兴趣的话请阅读 此文 ,并下载 Joda-Time 使用。

JSR-310

众所周知 Java 的规范就是多、而且啰嗦,这帮老大们(Export Group 中除了有 Oracle 的人,还有 IBM、Google 和 RedHat 的人)终于再也无法忍受 Java 那么烂的日期 API 了,于是就有了 JSR-310(感兴趣的请移步),官方的描述叫做“This JSR will provide a new and improved date and time API for Java.”,目前的阶段还在“Early Draft Review 2”,有得等。

JSR-310 将解决许多现有 Java 日期 API 的设计问题。比如 Date 和 Calendar 目前是可变对象,你可以随意改变对象的日期或者时间,而 Joda 就将 DateTime 对象设计成 String 对象一样地不可变,能够带来线程安全等等的好处,因此这一点也将被 JSR-310 采纳。

很多 JSR 规范都是在程序员的诋毁和谩骂声中萌芽的,然后会有开源项目来尝试解决 Java 的这些弊端,最后就轮到 JSR 就去抄他们的实现。除了新的日期 API,再比如 JCache(JSR-107),你知道它抄了多少 EhCache 的东西么……

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《四火的唠叨》

14,261 次阅读

5 thoughts on “Java 的日期 API 真烂

  1. 您好:朋友介绍的,看了你的文章,写的很不错,很多观点不谋而合。
    但是在日期这一篇里有一点问题,跟您讨论一下。
    我认为在 Date 中取得月份时值范围从 0 到 11 这样的设计是非常高明的设计理念。
    垃圾的程序员会这么显示月份:1、2、3;
    优秀的程序员会这么显示月份:一月、二月、三月……
    January February March……
    如果在显示时我们定义这样的数组
    monthName = [“ 一月”, “ 二月”, “ 三月” … “ 十二月”]
    那么我们从 Date 中取得的月份 0~11 刚好是这个数组的下标,可以不加任何处理直接取得它们的名字 monthName[m]
    在大大简化代码的同时,使展现方式变得非常人性化。
    所以我认为这是一个非常高明的设计理念,非常高明!

  2. java.sql.Date 的设计是这样的,因为数据库里确实是这样设计的,怪不得 Java。另外月份从 0 开始则是延续了数组的作风,也怪不得 Java,虽然可读性不好。

发表评论

电子邮件地址不会被公开。

back to top