第8条:覆盖equals时遵守通用约定
如果不需要覆盖equals方法,那么就无需担心覆盖equals方法导致的错误。
什么时候不需要覆盖equals方法?
1.类的每个实例本质上是唯一的。
例如对于Thread,Object提供的equals实现正好符合。
2.不关心类是否提供了“逻辑相等”的测试功能。
例如Random类提供产生随机数的能力,如果覆盖equals,目的该是检查两个Random实例是否产生了相同的随机数列,但实际上这个比较功能是不需要的,所以从Object继承的equals是足够的。
3.超类已经覆盖了euqlas,从超类继承过来的行为对于子类也是合适的。
例如,Set实现都从AbstractSet继承euqlas实现,List实现从AbstractList继承equals实现,Map实现从AbstractMap继承equals实现。
4.类是私有的或者包级私有,可以确定它的equals方法永远不会被调用。
这种时候好的做法应该覆盖equals方法,以防被意外调用:
@Override
public boolean equals(Object o) {
throw new AssertionError();
}
什么时候应该覆盖equals方法?
类具有自己特有的“逻辑相等”概念(不同于对象等同),而且超类没有覆盖equals以实现期望行为,这时需要覆盖equals方法。
通常这种类是“值类”,仅仅表示值的类,如Integer,Date,在利用equals方法时比较对象引用时,希望知道它们在逻辑上是否相等(值是否相等),而不是它们是否指向同一个对象。
一种特殊的“值类”,实例受控确保“每个值至多只存在一个对象”的类,如枚举类型,对于这样的类,逻辑等同域对象等同是同样的,因此Object的equals方法就能满足,无需覆盖。
equals有一系列的通用约定,在覆盖equals方法时,必须遵守这些约定,否则在使用jdk提供的映射表,集合等类时会导致奇怪的错误。
1.自反性,对于任何非null的引用值x,x.equals(x)必须返回true。
2.对称性,对于任何非null的引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true。
3.传递性:对于任何非null的引用值x、y和z,如果x.equals(y)返回true,并且y.equals(z)返回true,那么x.equals(z)也必须返回true。
4.一致性:对于任何非null的引用值x和y,只要equals的比较操作在对象中所用信息没有被修改,那么多次调用x.equals(y)就会一致地返回true或一致地返回false。
对于任何非null的引用值x,x.equals(null)必须返回false。
解释约定:
1.自反性,要求对象必须等于自身,假如一个类违背这一点,把该类的实例添加到集合中,该集合的contain方法会告诉你该集合不包含刚刚添加的实例,这种情况一般不会出现。
2.对称性,对于任何两个对象是否相等,必须保持一致,考虑下面一个不区分大小写的字符串的类:
public final class CaseInsensitiveString {
private String s; public CaseInsensitiveString(String s) {
if(s == null) {
throw new NullPointerException();
}
this.s = s;
} @Override
public boolean equals(Object o) {
if(o instanceof CaseInsensitiveString) {
return s.equalsIgnoreCase(((CaseInsensitiveString) o).s);
}
if(o instanceof String) {
return s.equalsIgnoreCase((String) o);
}
return false;
} }
CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
String s = "polish";
System.out.println(cis.equals(s));
System.out.println(s.equals(cis));
cis.equals(s)返回true
s.equals(cis)返回false
问题在于CaseInsensitiveString类中的equals方法知道String对象,而String类中的equals方法却不知道CaseInsensitiveString,因此违反了对称性。
看看String的equals方法:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
String的equals不知道CaseInsensitiveString是一个不区分大小写的类,只是把它当成一个Object或String。
解决这个问题的方法是把与String互操作的这段代码从equals方法中去掉。
@Override
public boolean equals(Object o) {
return o instanceof CaseInsensitiveString &&
((CaseInsensitiveString) o).s.equalsIngnoreCase(s);
}
这样的CaseInsensitiveString的equals方法返回true必须它比较的对象是CaseInsensitiveString,如果比较对象不是CaseInsensitiveString,比如是String,那么它一定会返回false。
3.传递性,如果一个对象等于第二个对象,并且第二个对象又等于第三个对象,则第一个对象一定等于第三个对象。
考虑子类增加的信息会影响到equals的比较结果。
首先有一个简单的不可变的二维整数型的Point类:
public class Point {
private final int x;
private final int 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 p.x == x && p.y == y;
}
}
扩展这个类增加颜色信息:
public class ColorPoint extends Point {
private final Color color; public ColorPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
} @Override
public boolean equals(Object o) {
if(!(o instanceof ColorPoint))
return false;
return super.equals(o) && ((ColorPoint) o).color == color;
} }
如果直接从Point继承equals,颜色信息就会被忽略掉,所以覆盖equals实现颜色信息比较。
问题在于比较普通点和有色点时,调用普通点的equals去比较有色点,如果x,y相等,那么返回true,调用有色点的equals去比较普通点,总是返回false,不符合的对称性。
修正对称性:
@Override
public boolean equals(Object o) {
if(!(o instanceof Point))//如果比较对象不是Point或其子类,总是返回false
return false; if(!(o instanceof ColorPoint))//如果比较对象是普通点,使用普通点的比较方法
return o.equals(this); return super.equals(o) && ((ColorPoint)o).color == color;//如果是有色点,用Point的比较方法比较x和y同时比较颜色信息
}
这种方法提供了对称性,但是牺牲了传递性:
ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
p1.equals(p2)和p2.equals(p3)都返回true,但p1.equals(p3)则返回false,违反传递性。前面两种比较不考虑颜色,而第三种比较则考虑了颜色。
在equals方法中用getClass测试代替instanceof测试,可以扩展可实例化的类和增加新的组件,同时保留equals约定:
@Override
public boolean equals(Object o) {
if(o == null || o.getClass() != getClass())
return false;
Point p = (Point)o;
return p.x == x && p.y == y;
}
只有当对象具有相同的实现时,才能使对象等同,这样的话,p1.equals(p2),p2.equals(p3)和p1.equals(p3)都返回false,符合传递性
下面编写一个方法,检验某个整值点是否在单位圆中:
private static final Set<Point> unitCircle;
static {
unitCircle = new HashSet<Point>();
unitCircle.add(new Point(1, 0));
unitCircle.add(new Point(0, 1));
unitCircle.add(new Point(-1, 0));
unitCircle.add(new Point(0, -1));
} public static boolean onUnitCircle(Point p) {
return unitCircle.contains(p);
}
但是假设通过某种不添加值组件的方式扩展Point,例如让构造器记录创建了多少个实例:
public class CounterPoint extends Point {
private static final AtomicInteger counter = new AtomicInteger(); public CounterPoint(int x, int y) {
super(x, y);
counter.incrementAndGet();
} public int numberofCreated() {
return counter.get();
}
}
根据里氏替换原则,一个类型的重要属性也将适用于它的子类型,但是,如果将CounterPoint实例传给onUnitCircle方法,如果Point类使用了基于getClass的equals方法,无论CounterPoint的x和y值是什么,onUnitCircle都会返回false,但是如果在Point上使用基于instanceof的equals方法,当遇到CounterPoint时,相同的OnUnitCircle方法就会工作得很好。
所以没有一种方法可以满足既扩展不可实例化的类,又增加值组件。根据复合优先于继承原则,不再让ColorPoint扩展Point,而是在ColorPoint中加入一个私有的Point域,以及一个公共视图方法,此方法返回一个与该有色点处于相同位置的普通Point对象:
public class ColorPoint {
private final Point point;
private final Color color; public ColorPoint(int x, int y, Color color) {
if(color == null) {
throw new NullPointException();
point = new Point(x, y);
this.color = color;
} public Point asPoint() {
return point;
} @Override
public boolean equals(Object o) {
if(!(o instanceof ColorPoint) o;
return cp.point.equals(point) && cp.color.equals(color);
}
}
注意,可以在一个抽象类的子类中增加新的值组件,而不会违反equals约定,只要不可能直接创建超类的实例,前面的种种问题都不会发生。
4.一致性,如果两个对象相等,那么它们必须始终保持相等,除非它们有一个对象被修改了。可变对象在不同时候可以与不同的对象相等,而不可变对象则不能,相等的对象永远相等,不想等的对象永远不相等。
无论类是否可变,都不要使equals依赖于不可靠的资源。如java.net.URL的equals方法依赖于URL中主机IP地址的比较,而将一个主机名转成IP可能需要访问网络,而网络的资源是不确定的,所以无法保证产生相同结果。
显示地通过一个null测试来实现对于任何非null的引用值x,x.equals(null)必须返回false是不必要的:
@Override public boolean equals(Object o) {
if(o == null)
return false;
}
因为为了测试等同性,equals方法必须先把参数转换成适当的类型,以便可以调用它的访问方法,或者访问它的域,在进行转换之前,equals必须使用instanceof操作符来检查其参数是否为正确的类型。如果比较对象是null,在instanceof的类型检查测试就不可能通过。
实现高质量equals方法的诀窍:
1.使用==操作符检查”参数是否为这个对象的引用“,如果比较操作代价很大,就值的这么做。
2.使用instanceof操作符检查”参数是否为正确的类型“,一般来说正确的类型指equals方法所在的类,某些情况下,是指该类所实现的某个接口,如果类实现的接口改进了equals约定,允许在实现了该接口的类之间进行比较,就使用接口。集合接口如Set,List,Map具有这样的特性。
3.把参数转换成正确的类型。
4.对于类中每个”关键域“,检查参数中的域是否与该对象中对应的域相匹配,域的比较顺序可能会影响性能,应该最先比较最可能不一致的域,或者开销低的域,不属于对象逻辑状态的域一般不比较,如果”冗余域“代表了整个对象的综合描述,同时比较冗余域的开销比比较所有关键域的开销小,那么比较冗余域可以节省比较失败时去比较实际数据所需要的开销。
5.当覆盖了equals方法后,测试是否符合equals的通用约定。
6.覆盖equals时总要覆盖hashCode。
7.不要企图让equals方法过于智能,过度地寻求各种等价关系,容易造成麻烦,如File类不应该把指向同一文件的符号链接当作相同的对象来看待。
8.不要将equals声明中的Object对象替换为其他类型,这会造成没有覆盖,而是重载,只要两个方法返回同样结果,那么这样是可以接受的,但与增加的复杂性相比,不值得。
@Override注解可以防止本想覆盖而错写成重载的方法,如果你的目的是覆盖,就使用该注解,这样在你出错的时候,能提示你你写的方法并不是一个覆盖的方法。
第8条:覆盖equals时遵守通用约定的更多相关文章
- Effective Java —— 覆盖equals时遵守通用约定
本文参考 本篇文章参考自<Effective Java>第三版第十条"Obey the general contract when overriding equals" ...
- Effective Java 第三版——10. 重写equals方法时遵守通用约定
Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...
- 第8条:覆盖equals时请遵守通用约定
第8条:覆盖equals时请遵守通用约定 引言:尽管Object是一个具体类,但是设计它主要是为了拓展.它所有的非final方法(equals.hashCode.toString.clone和fina ...
- Item 8 覆盖equals时请遵守通用约定
在覆盖equals方法的时候,你必须要遵守它的通用约定,不遵守,写出来的方法,会出现逻辑错误.下面是约定的内容: equals方法实现了等价关系: 自反性.对于任何非null的引用值,x.eq ...
- 覆盖equals方法时请遵守通用约定
覆盖equals方法时请遵守通用约定 覆盖equals方法看起来很简单,但是有许多覆盖方式会导致错误,并且后果很严重.最容易避免这种类问题的方法就是不覆盖equals方法,在这种情况下,类的每个实 ...
- Item 9 覆盖equals时总要覆盖hashCode
为什么覆盖equals时,总要覆盖hashCode? 原因是,根据Object规范: 如果两个对象根据equals(Object)方法比较是相等的,那么调用这两个对象中任意一个对象的hashCod ...
- 第八条:覆盖equals时请遵守通用约定
==是物理相等 equals是逻辑相等 因为每个类的实例对象本质上都是唯一的 ,利用物理相等(==)是指一个实例只能相等于它自己. 利用逻辑相等是(equals)指 一个实例是否和另一个实例的某些关键 ...
- 覆盖equals时请遵守通用约定
Object类中非final修饰的方法有equals().hashCode().toString().finalize().clone()1.equals()方法不需要被覆盖的情况:1)实例化的对象只 ...
- 第10项:重写equals时请遵守通用约定
重写equals方法看起来似乎很简单,但是有许多重写方式会导致错误,而且后果非常严重.最容易避免这类问题的办法就是不覆盖equals方法,在这种情况下,类的每个实例都只能与它自身相等.如果满足了以 ...
随机推荐
- Eclipse+Maven构建web项目及部署时Maven lib依赖问题的解决
目录 Eclipse中m2e插件构建web项目的步骤 Maven工具构建web项目再导入Eclipse的步骤 [一].Eclipse中m2e插件构建web项目的步骤 第一步:创建项目,按照 New – ...
- Chord算法(原理)
Chrod算法是P2P中的四大算法之中的一个,是有MIT(麻省理工学院)于2001年提出,其它三大算法各自是: CAN Pastry Tapestry Chord的目的是提供一种能在P2P网络高速定位 ...
- ListOrderedMap
要有序能够用List,要便于查找能够用Map,那既要有序又便于查找呢? 近期我就遇到了这样一个问题.Java没有给我们提供现成的类.我们全然能够自己开发个类继承List和Map(Java原来就有不能够 ...
- Ubuntu下配置 keepalived+nginx+tomcat 负载均衡
本文力图阐述在 Ubuntu Server 环境下使用 Keepalived + Nginx + Tomcat 搭建高可用负载均衡环境的操作步骤和简约配置,这里不涉及性能调优.先说一下他们各自扮演的角 ...
- [001]const和指针
很经典的: const int* p: int* const p: 前者表示指针指向的值是const,指向的值不可变,但是指针本身是可变的:后者表示改指针是const,指针不可变,但是指向的值是可变的 ...
- iOS 如何进行逆向工程
原文:http://www.zhihu.com/question/20317296 季逸超,Peak-Labs创始人/CEO,猛犸浏览器.Rasgue- 有幸被邀请回答,不过不知道您要了解的'系统机制 ...
- Migration from Zend Framework v2 to v3
Migration from Zend Framework v2 to v3 Zend Framework v2 to v3 has been intended as an incremental u ...
- 正则表达式 之 C#后台应用
正则表达式在.Net就是用字符串表示,这个字符串格式比较特殊,无论多么特殊,在C#语言看来都是普通的字符串,具体什么含义由Regex类内部进行语法分析. Regex 类 存在于 System.Text ...
- VIM 分割窗口
VIM 分割窗口 *08.1* 分割窗口 打开新窗口最简单的命令如下: :split 这个命令把屏幕分解成两个窗口并把光标置于上面的窗口中: +----------------------- ...
- Oracle database server 安装tips
需要手动解压第二个包的文件合并到第一个包的相同目录中. 以12c为例,需要把 winx64_12102_SE2_database_1of2.zip和winx64_12102_SE2_database_ ...