Effective Java笔记之改写equals的通用约定
2015-11-27 11:29
549 查看
改写equals的通用约定
我们知道,在java的世界里,所有的类都是Object的派生类,其实Java设计Object的缘由就是为了扩展,它的所有非fina方法,包括equals、hashCode、toString和finalize都有明确的通用约定。任何一个改写这些方法的时候,都得遵守这些约定。改写equals方法看起来非常简单,但是许多改写的方式会导致错误,而且后果很严重。要避免问题最简单的方法就是不改写equals方法,在这种情况下,每个实例只与自己相等。以下情况,不需要改写equasl方法,
一个类的每个实例本质上都是唯一的。对于代表了活动而不是值的类,比如Thread类。
不关心一个类是否提供了逻辑相等的测试功能。
超类已经改写了equals,从超类继承过来的行为对于子类也是合适的。例如,大多数的Set都继承了AbstractSet的equals实现,类似的还有List和Map。
一个类是私有的,并且确认它的equals方法永远不会调用。
那么,什么时候,应该改写equals呢?当一个类有自己特定的“逻辑相等”概念,而超类并没有改写equals的情况下。这通常适用于“值类”情况下。比如,我们想比较两个实例是否在值一样,而不是他们是否指向同一个对象。
有一种值类对象是不需要改写equals方法的,即类型安全枚举类型。
比如,我们有以下代码,
public class Point { private int x; private int y; public Point(int x, int y) { this.x = x; this.y = y; } public static void main(String[] args) { Point p1 = new Point(2, 5); Point p2 = new Point(2, 5); System.out.println("p1 equals p2? " + p1.equals(p2)); } }
我们在Point中,并没有重写equals方法,当我们想要判断p1和p2在值上是否相等的时候,我们判断的是他们的地址是否一样(也就是是否指向同一个对象),这是由于其调用了父类(Object)的equals方法,由于p1和p2是两个实例,所以p1.equals(p2)的结果是false。显然不是我们预期的结果,所以我们得重写equals方法。
改写前,就让我们来了解一下equals的一些约定,
equals方法实现了等价关系;
自反性。对于任意的x,x.equals(x)一定为true;
对称性。对于任意的x,y,x.equals(y)和y.equals(x)的值是一样的。
传递性。对于任意的x,y,z,若x.equals(y)为true,y.equals(z)也为true,则x.equals(z)也为true。
一致性。对于任意的x,y,如果x和y没有被修改,则多次调用x.equals(y)的结果是一样的。
非空性。对于任意的x,x.equals(null)一定是false。
让我们来逐条分析,
对于自反性,相比没有啥可以说的。如果自己和自己都不想等的话,那一切不都乱套了么?
对于对称性,其意思是,在x,y是否相等这个问题上,是一致的,不能说x等于y,但y不等于x。
让我们来看下面一个例子,
public class CaseInsensitiveString { private String s; public CaseInsensitiveString(String s) { if (s == null) throw new NullPointerException(); this.s = s; } @Override public boolean equals(Object obj) { // TODO Auto-generated method stub if (obj instanceof CaseInsensitiveString) return s.equalsIgnoreCase(((CaseInsensitiveString) obj).s); if (obj instanceof String) return s.equalsIgnoreCase((String) obj); return false; } public static void main(String[] args) { CaseInsensitiveString cis = new CaseInsensitiveString("wangfabo"); String s = "WangFaBo"; System.out.println(cis.equals(s)); System.out.println(s.equals(cis)); } }
上面的例子,我们可以分析cis.equals(s)的结果是true,但是s.equals(cis)结果确是false,这是因为String的equals方法并没有实现不区分大小写。所以上个例子的equals违反了传递性规则,会给程序带来错误。
对于传递性,它的意思是,如果一个对象等于第二个对象,第二个对象等于第三个对象,则第一个对象等于第三个对象,很好理解。考虑这样的情形:一个程序员创建了一个子类,它为超类增加了一个新的特征(变量),那么,新的特征就会影响到equals的比较结果。
超类Point代码,
public class Point { private int x; private int y; public Point(int x, int y) { this.x = x; this.y = y; } @Override public boolean equals(Object obj) { if (!(obj instanceof Point)) return false; Point p = (Point) obj; return p.x == x && p.y == y; } }
子类ColorPoint代码,
public class ColorPoint extends Point { private Color color; public ColorPoint(int x, int y, Color color) { super(x, y); this.color = color; } }
我们要给ColorPoint添加一个equals方法,如果,你完全不提供的话,那么就会调用Point的equals方法,这样就完全忽略了color变量,显然不符合实际,假设我们这样写equals,
public boolean equals(Object obj) { if (!(obj instanceof ColorPoint)) return false; ColorPoint cp = (ColorPoint) obj; return super.equals(obj) && cp.color == color; }
看似很符合逻辑,当不是ColorPoint实例,就返回false,否则,比较x和y是否相等(调用super.equals方法)、和判断Color是否一样。
但是,考虑以下情况,
Point p = new Point(2, 5); ColorPoint cp = new ColorPoint(2, 5, Color.red);
当我们比较p.equals(cp)时,会调用Point的equals方法,会将cp强制转换为Point类型,然后判断x与y是否分别一致,结果是true;
但是当比较cp.equals(p)时,由于p并不是ColorPoint的实例,所以会返回false。
所以这个equals方法违背了第二个约定,对称性。
我们可以修改代码,让equals方法接收Point类型,
public boolean equals(Object obj) { // TODO Auto-generated method stub if (!(obj instanceof Point)) return false; if (!(obj instanceof ColorPoint)) return obj.equals(this); ColorPoint cp = (ColorPoint) obj; return super.equals(obj) && cp.color.equals(color); }
我们可以测试,p.equals(cp)和cp.equals(p)的结果都是true。但是这又带来了另一个问题,考虑如下实例,
Point p = new Point(2, 5); ColorPoint cp1 = new ColorPoint(2, 5, Color.red); ColorPoint cp2 = new ColorPoint(2, 5, Color.blue); System.out.println(p.equals(cp1)); System.out.println(p.equals(cp2)); System.out.println(cp1.equals(cp2));
我们可以得到p和p1相等,p和p2相等,由于传递性,我们可以得到p1和p2相等,但是结果却是false(不相等)。这是,由于,在进行Point和ColorPoint比较的时候,会牺牲掉ColorPoint的属性,所以只要ColorPoint的坐标属性和Point的坐标属性一样,就判断为相等,这显然是不对的。
怎么解决呢?根据Java的一个设计原则:复合优于继承,我们可以不让ColorPoint继承Point,而是使用Point,代码如下,
public class ColorPoint { private Point point; private Color color; public ColorPoint(int x, int y, Color color) { point = new Point(x, y); this.color = color; } public Point asPoint(){ return point; } public boolean equals(Object obj) { // TODO Auto-generated method stub if (!(obj instanceof ColorPoint)) return false; ColorPoint cp = (ColorPoint) obj; return cp.point.equals(point) && cp.color.equals(color); } public static void main(String[] args) { Point p = new Point(2, 5); ColorPoint cp1 = new ColorPoint(2, 5, Color.red); ColorPoint cp2 = new ColorPoint(2, 5, Color.blue); System.out.println(p.equals(cp1)); System.out.println(p.equals(cp2)); System.out.println(cp1.equals(cp2)); } }
这样,代码就可以通过测试,符合传递性和对称性。
对于一致性和非空性,就不在多谈。较好理解。
本篇博客就写到这里,下篇介绍改写hashCode的通用约定。
相关文章推荐
- java对世界各个时区(TimeZone)的通用转换处理方法(转载)
- java-注解annotation
- java-模拟tomcat服务器
- java-用HttpURLConnection发送Http请求.
- java-WEB中的监听器Lisener
- Android IPC进程间通讯机制
- Android Native 绘图方法
- Android java 与 javascript互访(相互调用)的方法例子
- Python动态类型的学习---引用的理解
- 介绍一款信息管理系统的开源框架---jeecg
- 聚类算法之kmeans算法java版本
- java实现 PageRank算法
- PropertyChangeListener简单理解
- 插入排序
- 冒泡排序
- 堆排序
- 快速排序
- 二叉查找树