Object类中有很多通用方法,比如equals
、toString
、hashCode
,还有实现了Comparable
的类,它们的方法都有明确的约定,如果你想你的类能与其他类良好的工作在一起,请遵守这些约定。
其实很多时候equals
方法根本不需要被覆盖:
只有当我们自己设计一个“值类”的时候,才需要实现equals方法。
x
,x.equals(x)==true
x,y
,x.equals(y) == y.equals(x)
x,y,z
,如果x.equals(y)==y.equals(z)==a
那么x.equals(z)==a
a是一个布尔值x,y
,只要多次调用过程中,equals使用的到的属性没有改变,那么多次调用的结果也不应该改变x
,x.equals(null)==false
这些规范看着有点让人感觉像是回到了数学课上,但是不遵循这些规范会带来一些潜在的后果。
对于自反性,如果一个类不能在equals中遵循自反性,那么Set的contains
方法就可能没法返回正常的值。集合中很可能包含很多完全相同的实例。
对于违反对称性,看下面的一个例子
class CaseInsensitiveString{ private final String s; public CaseInsensitiveString(String s){ this.s = s; } @Override public boolean equals(Object o) { if (this == o) return true; if (o instanceof String) return s.equalsIgnoreCase((String) o); if (o instanceof CaseInsensitiveString) return s.equalsIgnoreCase(((CaseInsensitiveString) o).s); return false; } }
public class EqualsTest { public static void main(String[] args) { String string = "HelloWorld"; CaseInsensitiveString ciString = new CaseInsensitiveString("helloworld"); System.out.println(ciString.equals(string)); System.out.println(string.equals(ciString)); } }
CaseInsensitiveString
使用委托实现了一个对大小写不敏感的字符串类。如果你运行这段程序,你会发现,主函数中的第一条输出语句是true
,第二条是false
,这已经违反了对称性。
原因不难看出,CaseInsensitiveString
的equals
方法第二行做了一个画蛇添足的操作,如果你传入一个String
对象,它仍然会按照忽略大小写的模式进行对比,但如果你用String
的实例去和CaseInsensitiveString
对比,显然,String
肯定不知道它是个什么牛马,直接返回false。看似一个聪明的,使该类支持原生String
的做法,却可能会酿成大祸。
CaseInsensitiveString
这个不明智的做法可能使他在不同的集合中产生不同的效果,例如如下的代码,它返回什么呢?
List<CaseInsensitiveString> list = new ArrayList<>(); list.add(ciString); System.out.println(list.contains(string));
完全取决于集合中contains
方法调用equals
的前后顺序。
解决问题很简单,别耍这种小聪明就行了。
违反传递性通常出现在子类和父类的比较中。
class Point{ private int x,y; public Point(int x,int y){ this.x = x;this.y = y; } @Override public boolean equals(Object o){ if (!(o instanceof Point)) return false; Point p = (Point) o; return x == p.x && y == p.y; } }
class ColorPoint extends Point{ private int color; public ColorPoint(int x, int y,int color) { super(x, y); this.color = color; } @Override public boolean equals(Object o){ if (o instanceof ColorPoint) return super.equals(o) && color == ((ColorPoint)o).color; if (o instanceof Point) return super.equals(o); return false; } }
public class EqualsTest { public static void main(String[] args) { ColorPoint colorPoint1 = new ColorPoint(1,2,0xff0000); Point point = new Point(1,2); ColorPoint colorPoint2 = new ColorPoint(1,2,0xffffff); System.out.println(colorPoint1.equals(point)); System.out.println(point.equals(colorPoint2)); System.out.println(colorPoint1.equals(colorPoint2)); } }
这段代码违反了传递性,造成问题的原因是ColorPoint
在和Point
类型比较的时候,忽略了颜色信息。
这个问题似乎无法解决,如果你想让Point
和Point
的子类能够判等的话,那就永远无法绕过Point
没有子类新增加的属性的问题。
一个可选的办法就是不适用继承,而采取组合,并提供一个父类对象的视图,如何判断,全凭用户取舍:
class ColorPoint2 { private final Point point; private final int color; public ColorPoint2(Point point,int color){ this.point = point; this.color = color; } public Point asPoint(){ return point; } @Override public boolean equals(Object o){ if (!(o instanceof ColorPoint)) return false; ColorPoint2 c = (ColorPoint2) o; return point.equals(c.point) && color == c.color; } }
在一个抽象类的子类中增加新的属性就不会出现这种问题。因为你无法创建这个抽象的父类。
java类库中URL类的实现就没遵循一致性,因为它比较时依赖了网络资源。
// URL.equals中调用了handler.equals进行判断两个URL是否相等 public boolean equals(Object obj) { if (!(obj instanceof URL)) return false; URL u2 = (URL)obj; return handler.equals(this, u2); } // handler.equals 中调用了sameFile判断了是否是同一个文件 protected boolean equals(URL u1, URL u2) { String ref1 = u1.getRef(); String ref2 = u2.getRef(); return (ref1 == ref2 || (ref1 != null && ref1.equals(ref2))) && sameFile(u1, u2); } // handler.sameFile 做了很多确认操作,我这里省略了,最后它使用hostEquals判断了两个URL的主机是否一致 protected boolean sameFile(URL u1, URL u2) { // ...省略不重要代码 // Compare the hosts. if (!hostsEqual(u1, u2)) return false; return true; } // handler.hostEqual 中进行了一些网络操作,将URL转换成host地址 protected boolean hostsEqual(URL u1, URL u2) { InetAddress a1 = getHostAddress(u1); InetAddress a2 = getHostAddress(u2); // if we have internet address for both, compare them if (a1 != null && a2 != null) { return a1.equals(a2); // else, if both have host names, compare them } else if (u1.getHost() != null && u2.getHost() != null) return u1.getHost().equalsIgnoreCase(u2.getHost()); else return u1.getHost() == null && u2.getHost() == null; }
问题在于,随着时间,这个URL很可能被绑定到其它的主机上,原来的u1.equals(u2)
可能和之后的u1.equals(u2)
产生不同的结果。
所以equals
中不要依赖不确定不可靠的资源进行判断。
很多时候我们为了保证非空性会写这样的代码:
@Override public boolean equals(Object o){ if(o==null)return; if(o instanceof Clz){...} return false; }
其实这个方法的第一行是没用的,因为instanceof已经会帮助你判空了。它在o为null的时候会返回false。
@Override public boolean equals(Object o){ if (this == o)return true; if (!(o instanceof Point)) return false; Point p = (Point) o; return x == p.x && y == p.y; }
这也是很多IDE自带的生成工具的写法。
...未完