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 来替换, 这就符合里氏代换原则
错误的范例
违反对称性原则
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();
}
}
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
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 类违反了一致性原则.