JDK BUG吗? 混乱的日期API

首先看一个测试用例:

import org.junit.Assert;
import org.junit.Test;

import java.sql.Time;
import java.sql.Timestamp;
import java.util.Date;

/**
 * <p>User: Zhang Kaitao
 * <p>Date: 13-5-26 下午5:43
 * <p>Version: 1.0
 */
public class DateTest {
    //when only millisecond part is different

    @Test
    public void testDateAfter() {
        Date d1 = new Date(1369461400000L);
        Date d2 = new Date(1369461400001L);
        Assert.assertTrue(d2.after(d1));
    }

    @Test
    public void testTimestampAfterOK() {
        Timestamp d1 = new Timestamp(1369461400000L);
        Timestamp d2 = new Timestamp(1369461400001L);
        Assert.assertTrue(d2.after(d1));
    }

    @Test
    public void testTimestampCastToDateAfterFail() {
        Date d1 = new Timestamp(1369461400000L);
        Date d2 = new Timestamp(1369461400001L);
        Assert.assertFalse(d2.after(d1));
    }

    @Test
    public void testDateCompare() {
        Date d1 = new Date(1369461400000L);
        Date d2 = new Date(1369461400001L);
        Assert.assertTrue(d2.compareTo(d1) == 1);
    }

    @Test
    public void testTimestampCompareOK() {
        Timestamp d1 = new Timestamp(1369461400000L);
        Timestamp d2 = new Timestamp(1369461400001L);
        Assert.assertTrue(d2.compareTo(d1) == 1);
    }

    @Test
    public void testTimestampCastToDateCompareOK() {
        Date d1 = new Timestamp(1369461400000L);
        Date d2 = new Timestamp(1369461400001L);
        Assert.assertTrue(d2.compareTo(d1) == 1);
    }

}

大家可能看到testTimestampCastToDateAfterFail测试用例,d2.after(d1) 是false。

从网络上找了下,类似的bug如下:

http://bugs.sun.com/view_bug.do?bug_id=5008227

写道

2004-06-14
EVALUATION

This is a side effect caused by the 4340146 fix. Because Date.after() no longer calls getTime(), after() and equals() in Timestamp work compare different time values.

Timestamp.after and before should call compareTo which works correctly.
###@###.### 2004-03-15

其也是建议使用compareTo,而不是after/before。

还一篇是在stackoverflow上的:

http://stackoverflow.com/questions/15629222/java-sql-timestamp-comparison-bug

有一个compareTo的,也有过类似的问题,不过1.5已经修复。

http://icedtea.classpath.org/bugzilla/show_bug.cgi?id=676

http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=5103041

其中的主要问题是:

Timestamp没有重载(public boolean after(Date d) );

Date中的fastTime 存储了毫秒值;但Timestamp的fastTime只存储到秒,毫秒值部分存储到nanos部分。具体细节可参考jdk代码。

有细心的朋友可能注意到了:我的d1 和 d2 实际上是Timestamp类型啊

    @Test
    public void testTimestampCastToDateAfterFail() {
        Date d1 = new Timestamp(1369461400000L);
        Date d2 = new Timestamp(1369461400001L);
        Assert.assertFalse(d2.after(d1));
    }

那比较的时候,怎么会发生这种事情?而且有朋友还注意到了compareTo就没有这个问题。

首先看下jdk的文档:

注:此类型由 java.util.Date 和单独的毫微秒值组成。只有整数秒才会存储在 java.util.Date 组件中。小数秒(毫微秒)是独立存在的。传递不是 java.sql.Timestamp 实例的对象时,Timestamp.equals(Object) 方法永远不会返回 true,因为日期的毫微秒组件是未知的。因此,相对于 java.util.Date.equals(Object) 方法而言,Timestamp.equals(Object) 方法是不对称的。此外,hashcode 方法使用底层 java.util.Date 实现并因此在其计算中不包括毫微秒。

鉴于 Timestamp 类和上述 java.util.Date 类之间的不同,建议代码一般不要将 Timestamp 值视为 java.util.Date 的实例。Timestamp 和 java.util.Date 之间的继承关系实际上指的是实现继承,而不是类型继承。

此处 可能已经注意到了:

1、Timestamp 和 java.util.Date 之间的继承关系实际上指的是实现继承,而不是类型继承。

2、Timestamp.equals(Object) 方法是不对称的。此外,hashcode 方法使用底层 java.util.Date 实现并因此在其计算中不包括毫微秒。

此处我们大体能概括出来:

1、Date和Timestamp并不是继承关系。。。。。

2、after方法也是双向不对称的。。。。

关于不对称,再来看两个测试用例:

    @Test
    public void testTimestampAfterOK2() {
        Date d1 = new Timestamp(1369461400000L);
        Timestamp d2 = new Timestamp(1369461400001L);
        Assert.assertFalse(d2.after(d1));
    }

    @Test
    public void testTimestampAfterOK3() {
        Timestamp d1 = new Timestamp(1369461400000L);
        Date d2 = new Timestamp(1369461400001L);
        Assert.assertFalse(d2.after(d1));
    }

主要原因是Timestamp没有重载(public boolean after(Date d) );而且仔细思考了下,如果从JDK文档上总结的话,不应该算作bug;但是从compareTo上看那就应该是bug。

真是混乱啊。。。 JSR 310 新的日期和时间API 在JDK8会添加进去

更好的选择是使用如joda-time/或者使用JSR-310。

比较时应该注意自己的情况,如果不知道当前类型(Date/Timestamp)那么请使用compareTo;是什么使用日期,应该要做好单元测试。有了单元测试,才有了保险。。。。

此处如果你的java.sql.Time,JDK也没有提供只比较Time部分的API。。。。。。。

commons-lang也没有提供类似的API,不过commons-lang也在犹豫是否添加:

写道

DateUtils.isBeforeDay

DateUtils.isAfterDay

https://issues.apache.org/jira/browse/LANG-400

=================分割线==================================================

关于mysql的Timestamp:

假设表结构是:

create table `personal_message`(
  `id`               bigint not null auto_increment,
  `sender_id`        bigint,
  `receiver_id`      bigint,
  `send_date`        timestamp,
}

如果有人执行:

update receiver_id=1 where id=?

你可能会发现:你的send_date改成了当前时间!具体原因仔细看mysql官方文档,官方文档说的很明白:

http://dev.mysql.com/doc/refman/5.6/en/timestamp-initialization.html

因为像send_date发送时间,一旦确定是不需要改的,解决方案是只加个默认值:

`send_date`        timestamp default 0,

而且mysql还一个问题是timestamp不是存储到毫秒值,所以如果想存到毫秒值级别 请使用如bigint直接存储毫秒值。

=================分割线==================================================

jpa中映射日期类型,可以使用:

@Temporal(TemporalType.TIMESTAMP)
private Date sendDate;

TemporalType表示日期类型,分别对应:

public enum TemporalType {
	/**
	 * Map as <code>java.sql.Date</code>
	 */
	DATE,

	/**
	 * Map as <code>java.sql.Time</code>
	 */
	TIME,

	/**
	 * Map as <code>java.sql.Timestamp</code>
	 */
	TIMESTAMP
}

如果想在hibernate中映射其他日期类型,如Calendar:

可以使用hibernate的@org.hibernate.annotations.Type,如@Type(type = “timestamp”),默认支持的是:

写道

date, time, timestamp

Type mappings from java.util.Date and its subclasses to SQL types DATE, TIME and TIMESTAMP (or equivalent).

calendar, calendar_date Type mappings from java.util.Calendar to SQL types TIMESTAMP and DATE (or equivalent).

http://docs.jboss.org/hibernate/orm/4.2/manual/en-US/html_single/#mapping-types

当然,你也可以选择如joda-time,已经有hibernate集成了:

https://github.com/JodaOrg/joda-time-hibernate

如果你存储到数据库的是毫秒值,取回来想变成日期,可以自定义UserType,这个可以参考:

Hibernate自定义类型 集合—>字符串 存储

对于hibernate 写原生SQL时,还需要注意这个问题:《hibernate createSQLQuery的问题》,解决方案是:

如果hibernate4  addScalar(“m_apiendtime “,TimestampType.INSTANCE)
其他 addScalar(“m_apiendtime “,Hibernate.TIMESTAMP)

如果有些拿不准的,可以考虑上单元测试,好处多多。



相关文章

发表评论

Comment form

(*) 表示必填项

还没有评论。

跳到底部
返回顶部