Timestamp的equals不对称性

Timestamp

最近写代码的时候用了java.sql.Timestamp 类(容易存DB),遇到一个bug,查了很久才发现原因。

        Date date = new Date();
        Timestamp t1 = new Timestamp(date.getTime());

        System.out.println("Date equals Timestamp ? : " +  date.equals(t1));// true
        System.out.println("Timestamp equals Date ? : " +  t1.equals(date));// false
第一行,输出 true
第二行,输出 false

对,你没看错。只是将两个变量调换了位置而已。结果却不一样。

Timestamp的不对称性

查看资料发现说:Timestamp的equals()实现违反了对称性

官方文档也有这样一段话:

This type is a composite of a java.util.Date and a separate nanoseconds value. Only integral seconds are stored in the java.util.Date component. The fractional seconds - the nanos - are separate. The Timestamp.equals(Object) method never returns true when passed an object that isn't an instance of java.sql.Timestamp, because the nanos component of a date is unknown. As a result, the Timestamp.equals(Object) method is not symmetric with respect to the java.util.Date.equals(Object) method. Also, the hashCode method uses the underlying java.util.Date implementation and therefore does not include nanos in its computation.

意思就是:

该类型是java.util.Date 和一个单独的纳秒值的组合。只有整数秒存储在java.util.Date中。分数秒——纳米级——是分开存的。当传递一个不是java.sql.Timestamp的实例对象时,Timestamp.equals(Object)方法永远不会返回true,因为日期的nanos组件是未知的。因此,Timestamp.equals(Object)方法与java.util.Date.equals(Object)方法不对称。此外,hashCode方法使用底层java.util.Date的实现,因此在计算中不包括nanos。

Object的equals

一开始觉得这是对 Object.equals()的错误实现造成的。Object类的equals()方法很常见用的也很多,有时候我们也会自己覆写它。不过还是要遵守一些约定的。这里列下Object的规范要求:

自反性。对应任何非null的引用值x,x.equals(x) 必须返回true。
对称性。对于任何非null的引用值x和y,当且仅当y.equals(x) 返回true时,x.equals(y) 必须返回true。
传递性。对于任何非null的引用值x、y和z,如果x.equals(y) 必须返回true,且y.equals(z) 返回true,那么x.equals(z) 也必须返回true。
一致性。对于任何非null的引用值x和y,只要equals比较用到的信息没被改,多次x.equals(y)一致地返回true,或一致返回false。
对于任何非null的引用值x,x.equals(null)必须返回fasle

自反性
对于自反性,很好理解,等于自身。一般都不会违反。

对称性

上边的java.sql.Timestamp 例子就是违反了对称性。看下源码:

    // Timestamp
    @Override
    public boolean equals(java.lang.Object ts) {
        if (ts instanceof Timestamp) {
            return this.equals((Timestamp)ts);
        } else {
            return false;// 非Timestamp 实例直接返回false
        }
    }
    // 省略其他代码
    public boolean equals(Timestamp ts) {
        if (super.equals(ts)) {
            if  (nanos == ts.nanos) {
                return true;
            } else {
                return false;
            }
        } else {
            return false;
        }
    }

java.util.Date中

    // Date
    @Override
    public boolean equals(Object obj) {
        return obj instanceof Date && getTime() == ((Date) obj).getTime();
    }

Timestamp的equals 只与Timestamp的实例比较。Date的equals 比较两个实例的time值。

我们知道Timestamp 继承了 Date,在其基础上增加了nanos(纳秒)组件。简单来说,java.sql.Timestamp 是实现继承了java.util.Date,而不是类型继承。

思考修正

看到这,可能会怀疑是不是源码作者的失误?

反正我看了代码后,我第一想法是为啥不再加个判断java.util.Date?比如假设如下:

    @Override
    public boolean equals(java.lang.Object ts) {
        if (ts instanceof Timestamp) {
            return this.equals((Timestamp) ts);
        } else if (ts instanceof Date) {
            return this.getTime() == ((Date)ts).getTime();// 我们新加代码让它可以满足对称性
        }
        return false;// 非Timestamp 实例直接返回false
    }

推算了下,能解决对称性问题。别急着高兴。还需要验证下。

验证

看下面的代码先:

        Date date = new Date();
        Timestamp t1 = new Timestamp(date.getTime());

        System.out.println("Date equals Timestamp ? : " + date.equals(t1));
        System.out.println("Timestamp equals Date ? : " + t1.equals(date));
        
        Timestamp t2 = new Timestamp(date.getTime());
        t2.setNanos(t2.getNanos() + 1);// 给时间戳增加一纳秒
        System.out.println("Date equals Timestamp(Increase Nanos) ? : " + date.equals(t2));
        System.out.println("Timestamp equals Timestamp(Increase Nanos) ? : " + t1.equals(t2));


        System.out.println("date : " + date.getTime());// 三个 getTime()相同
        System.out.println("t1 : " + t1.getTime());
        System.out.println("t2 : " + t2.getTime());
在假设的基础上 date.equal(t1)为true, t1.equals(date)也变true(基于上边改编的代码),而date.equals(t2)为true,但是 t1.equals(t2)返回false。显然这不是我们期望的(毕竟t2比t1多了一纳秒)。它违反了传递性

从上边的验证上可以看出,java.sql.Timestamp 继承了java.util.Date。Timestamp的equals 方法违反了对称性,如果不违反对称性,就要违反传递性。问题估计更严重。

解释

对此,一直感觉无能为力。直到后来在一本书上看到下面一段话:

我们无法在扩展可实例化的类的同时,既增加新的组件,同时又保留equals 约定,除非愿意放弃面向对象的抽象所带来的优势。

个人理解,这是继承带来的影响,没法解决。写代码时尽量不要父子类的实例间不要混合使用。

hashCode

在写Java的时候,多多少少都会看到这个提示:覆盖equals 时总要覆盖 hashCode。
其实为了更好的利用HashMap、HashSet和HashTable集合,规范也有三条 hashCode的约定。
如果对象equals操作用到的信息没有改变,hashCode 必须始终如一地返回同一个整数。
如果两个对象利用equals相等,则两个对象的hashCode 必须相同。
如果两个对象利用equals不相等,则两个对象的hashCode 不一定要不相同。


看个反例:

一个简单的点 类。横竖坐标。

public class MyPoint {
    private int x;
    private int y;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof MyPoint)) {
            return false;
        }
        MyPoint myPoint = (MyPoint) o;
        return x == myPoint.x && y == myPoint.y;
    }
}

然后用HashMap进行了存储:

    public static void main(String[] args) {
        Map<MyPoint, String> map = new HashMap<>();
        MyPoint first = new MyPoint(1, 1);
        map.put(first, "First Point");

        MyPoint temp = new MyPoint(1, 1);
        System.out.println(first.equals(temp));// true
        System.out.println(map.get(temp));// 输出 null
    }

不可意思的是输出 null!这就有问题了,用两个equals 相等的点,取不到我要的value。

HashMap的结构
解释这个这就要说下HashMap 的数据结构了。
在这个散列中,HashMap的get0(key) 会计算 key的 hasCode,然后决定在哪个桶中进行查找。


上边的first 和 temp 两个point equals相等,但是 hasCode 不相等(未被覆写)。这就导致,put()方法把 "First Point" 放在了 n 桶中,但是 get() 却在 m 桶中进行查找。

因此,就要小心地覆写hashCode 方法了。一般IDE都会帮我们生成一个 hashCode方法。比如上边的类:

    @Override
    public int hashCode() {
        return Objects.hash(x, y);
    }
此时,再运行上边的代码就能获取到 "First Point" 了。

总之,覆写equals 方法时,记得一定要覆写hashCode 方法。


阅读更多
个人分类: Java
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭