equals 与里氏代换原则

equals 的实现原则

自反性(Reflexive) : a.equals(a) 必须为 true
对称性(Symmetric) : a.equals(b) && b.equals(a) 必须为 true
传递性(Transitive) : a.equal(b) && b.equals(c) && a.equals(c) 必须为 true
一致性(Consistent) : 多次调用 a.equal(b) 结果不变

里氏代换原则

If S is a subtype of T, then objects of type T may be replaced with objects of type S (i.e., objects of type S may substitute objects of type T) without altering any of the desirable properties of that program (correctness, task performed, etc.)

如果 S 是 T 的子类, 那么 T 在被 S 替代的情况下, 不需要修改任何程序的代码.
例如:

pubic void process(List<Integer> list) {
    for (Integer i : list) {
    System.out.println(i);
  }
}

此处的 list 我们可以使用任意的 ArrayList 或者 LinkedList 来替换, 这就符合里氏代换原则

错误的范例

违反对称性原则

Human.java
public class Human {
    private String name;

    public Human(String name) {
        this.name = name;
    }
    
    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Human)) return false;
        if (o == this) return true;
        Human h = (Human) o;
        return name != null ? name.equals(h.name) : h.name == null;
    }

    @Override
    public int hashCode() {
        return name.hashCode();
    }
}
Man.java
package com.qunar.fresh;

public class Man extends Human {

    private int length;

    public Man(String name, int length) {
        super(name);
        this.length = length;
    }
    
    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Man)) return false;
        if (this == o) return true;
        Man m = (Man) o;
        return super.equals(o) && this.length == m.length;
    }

    @Override
    public int hashCode() {
        return super.hashCode() * 31 + length;
    }
}

试想如下代码的执行结果

Human h = new Human("Jack");
  Man m = new Man("Jack", 20);
  System.out.println("h.equals(m) : " + h.equals(m));
  System.out.println("m.equals(h) : " + m.equals(h));

h.equals(m) : true
m.equals(h) : false

这是在子类中新增属性经常会出现的错误, 一般来讲, 在子类的 equals 中, 如果可以, 则只使用接口或父类型进行比较, 例如 ArrayList 的 equals

equals in ArrayList
public boolean equals(Object o) {
    if (o == this)
        return true;
    if (!(o instanceof List))
        return false;

    ListIterator<E> e1 = listIterator();
    ListIterator e2 = ((List) o).listIterator();
    while (e1.hasNext() && e2.hasNext()) {
        E o1 = e1.next();
        Object o2 = e2.next();
        if (!(o1==null ? o2==null : o1.equals(o2)))
            return false;
    }
    return !(e1.hasNext() || e2.hasNext());
}

同样的, 这个例子中, 对于里氏代换原则来讲, 也不成立, 例如如下代码

Human h = new Human("Jack");
Man m = new Man("Jack", 20);
Set<Human> hSet = new HashSet<Human>();
hSet.add(h);
System.out.println("Set contains Human : " + hSet.contains(h));
System.out.println("Set contains Man : " + hSet.contains(m));

Set contains Human : true
Set contains Man : false

容易发现这里 h.equals(m) 为 true, 但是却不能用 m 替换掉 h 来做查询.
这个道理用在 compareTo 上也是一样的, 如果子类的 compareTo 覆盖了父类的 compareTo, 并且加入了新的元素作为比较元素, 则也会违反对称性原则, 导致 a.compareTo(b) > 0, b.compareTo(a) < 0. 同时也会违反里氏代换原则, 导致在 TreeSet 中插入 a 或 a 的子类的时候顺序发生变化.

Java 中不合原则的类

Timestamp 与 Date

Java 中, Timestamp 和 Date 是一个反例, 因为 Timestamp extands Date, 而 equals 方法中却使用了 Timestamp 独有的变量, 例如:

Date now = new Date();
Timestamp t = new Timestamp(now.getTime());
System.out.println("now.equals(t) : " + now.equals(t));
System.out.println("t.equals(now) : " + t.equals(now));

now.equals(t) : true
t.equals(now) : false

显然, Timestamp 的 equals 方法是违反对称性的.

java.net.URL

URL 类的 equals 方法会使用 InetAddress 将 host 转为 ip 来比较两个 ip 是否相等, 则就使得在使用 equals 的时候会访问网络来获取 ip. 然而, 获得 ip 与你是用的的 DNS 服务有关, 在不同的地点, 也许同一个 host 会获得不同的 ip. 这就让 URL 类违反了一致性原则.