如何在Java中避免equals方法的隐藏陷阱(上)
上一篇 / 下一篇 2012-09-18 14:25:51 / 个人分类:Java
译者注:你可能会觉得Java很简单,Object的equals实现也会非常简单,但是事实并不是你想象的这样,耐心的读完本文,你会发现你对Java了解的是如此的少。如果这篇文章是 一份Java程序员的入职笔试,那么不知道有多少人会掉落到这样的陷阱中。原文转自http://www.artima.com/lejava /articles/equality.html 三位作者都是不同领域的大拿,有兴趣的读者可以从上面这个连接直接去阅读原文。
uO8q?*H `b0/_9_7t#Y\)\QG_!ET[0 摘要51Testing软件测试网r%_X(j"@?J!y-N
51Testing软件测试网+F jE2LYqQ!g*D#F2JX%H5w本文描述重载equals方法的技术,这种技术即使是具现类的子类增加了字段也能保证equal语义的正确性。51Testing软件测试网 ]d5S J:uM{6K
Q`fe q2Y3N]:g9D)R0 在《Effective Java》的第8项中,Josh Bloch描述了当继承类作为面向对象语言中的等价关系的基础问题,要保证派生类的equal正确性语义所会面对的困难。Bloch这样写到:51Testing软件测试网"?b,{1X6c$l `
51Testing软件测试网 f-hQ)QvY6c ZX除非你忘记了面向对象抽象的好处,否则在当你继承一个新类或在类中增加了一个值组件时你无法同时保证equal的语义依然正确。
"G~H D4B0+} B#vt8Tc q0 在《Programming in Scala》中的第28章演示了一种方法,这种方法允许即使继承了新类,增加了新的值组件,equal的语义仍然能得到保证。虽然在这本书中这项技术是在 使用Scala类环境中,但是这项技术同样可以应用于Java定义的类中。在本文中的描述来自于Programming in Scala中的文字描述,但是代码被我从scala翻译成了Java
3W+e#IY;n'l!e-`0D7MB"j;e%C+_0 常见的等价方法陷阱
0@3wQ%vA1g051Testing软件测试网MN/n+G8E5w"e X*}java.lang.Object 类定义了equals这个方法,它的子类可以通过重载来覆盖它。不幸的是,在面向对象中写出正确的equals方法是非常困难的。事实上,在研究了大量的Java代码后,2007 paper的作者得出了如下的一个结论:
];r*g9i?4y051Testing软件测试网O$~!E9|/M几乎所有的equals方法的实现都是错误的!
$[A9E%l:Pr0b#QM_&}H8u8^j0 这个问题是因为等价是和很多其他的事物相关联。例如其中之一,一个的类型C的错误等价方法可能意味着你无法将这个类型C的对象可信赖的放入到容器中。比 如说,你有两个元素elem1和elem2他们都是类型C的对象,并且他们是相等,即elem1.equals(elm2)返回ture。但是,只要这个 equals方法是错误的实现,那么你就有可能会看见如下的一些行为:51Testing软件测试网&[j0c^q8}h+nk
Set hashSet<C> = new java.util.HashSet<C>();51Testing软件测试网2G\#e;K7l]"Kw hashSet.add(elem1); w OI N dh\7}-y0hashSet.contains(elem2); // returns false! |
当equals重载时,这里有4个会引发equals行为不一致的常见陷阱:51Testing软件测试网?y V;Tg obd7V
51Testing软件测试网3jD+R9DC1sD1、定义了错误的equals方法签名(signature)Defining equals with the wrong signature.
.Tf:}]G#l9fn*SB8U0^051Testing软件测试网.OUd P6p't ]2、重载了equals的但没有同时重载hashCode的方法。Changing equals without also changing hashCode.
)` @^;bS.x4@2n0M`@(}e}P8x0 3、建立在会变化字域上的equals定义。Defining equals in terms of mutable fields.
:i W0e2W{'`(V%V051Testing软件测试网n$])[#}4` l4、不满足等价关系的equals错误定义Failing to define equals as an equivalence relation.
Yc]:N*XG[051Testing软件测试网"y(| wg,e,` XY在剩下的章节中我们将依次讨论这4中陷阱。
rQ S)Jt051Testing软件测试网*N d.Q%X#Rd8f'u陷阱1:定义错误equals方法签名(signature)
5M9z ruK#W:y@ j{02d1_5x t7r:C1P0 考虑为下面这个简单类Point增加一个等价性方法:
%N.F}C'}ajc0\!HN R%i(j%o2l0public class Point { zOe TX(Z9l0|z4m%WLWm0 private final int x;51Testing软件测试网*]]0n6QSo9B public Point(int x, int y) { public int getX() {51Testing软件测试网C?'U)I/o*sO9e6y H `W.f@I1Y0 public int getY() { i%i9Y-Y/~^7\Xo;i0 // ...51Testing软件测试网:M%g;`X"@$NqN*C] |
看上去非常明显,但是按照这种方式来定义equals就是错误的。
D!Vki SS0// An utterly wrong definition of equals51Testing软件测试网a;ZD~;a%fpublic boolean equals(Point other) {
e"dndehLI0 return (this.getX() == other.getX() && this.getY() == other.getY());51Testing软件测试网zCCq8xG.N
}
%u5J:_s"O051Testing软件测试网4g_ OS]
这个方法有什么问题呢?初看起来,它工作的非常完美:51Testing软件测试网e2]#LYV*B6D:L
51Testing软件测试网W8mLZ&[9O.W'I51Testing软件测试网|&LZ-q)Z0rM#BX L
,j"C$}@vyN0Point p1 = new Point(1, 2); Point q = new Point(2, 3);51Testing软件测试网\p bEV~']l 51Testing软件测试网q C.Th%Or]XSystem.out.println(p1.equals(p2)); // prints true51Testing软件测试网,T,\pW*b0@&{;d8{ 'o,Zq5tz!D0System.out.println(p1.equals(q)); // prints false l7P:b1Tz$c#C4t6s0 |
(vH7_ zy K0 然而,当我们一旦把这个Point类的实例放入到一个容器中问题就出现了:51Testing软件测试网!k tY;]5RA}o+S
51Testing软件测试网*V]_ps`&a`tp O*|SEa'Px"x@09B8X"ZW$LG^P0import java.util.HashSet; 7Akd&}%O-n\051Testing软件测试网|!e^.Q k)pFq;zHashSet<Point> coll = new HashSet<Point>();51Testing软件测试网8qb'[nv3{M'a System.out.println(coll.contains(p2)); // prints false51Testing软件测试网R vF w N7}2Y3L |
为什么coll中没有包含p2呢?甚至是p1也被加到集合里面,p1和p2是是等价的对象吗?在下面的程序中,我们可以找到其中的一些原因,定义p2a是一个指向p2的对象,但是p2a的类型是Object而非Point类型:
+F,r%U9g1K9V#}(P0R2y3H+N Wl0
DJ_Y\UKs*B;L'_0Object p2a = p2; |
Fo A"w$l;IXy7H0 现在我们重复第一个比较,但是不再使用p2而是p2a,我们将会得到如下的结果:51Testing软件测试网i7?[Zw+t@
51Testing软件测试网-q(e){'M6t D!M9x51Testing软件测试网;F%wtu*B UL4r$e
System.out.println(p1.equals(p2a)); // prints false |
到底是那里出了了问题?事实上,之前所给出的equals版本并没有覆盖Object类的equals方法,因为他的类型不同。下面是Object的equals方法的定义51Testing软件测试网b hz^5G
9l&p8w,t Tg6|051Testing软件测试网Mqe~0T&M^
public boolean equals(Object other) |
U5YJ9yK8ga0 因为Point类中的equals方法使用的是以Point类而非Object类做为参数,因此它并没有覆盖Object中的equals方 法。而是一种变化了的重载。在Java中重载被解析为静态的参数类型而非运行期的类型,因此当静态参数类型是Point,Point的equals方法就 被调用。然而当静态参数类型是Object时,Object类的equals就被调用。因为这个方法并没有被覆盖,因此它仍然是实现成比较对象标示。这就 是为什么虽然p1和p2a具有同样的x,y值,”p1.equals(p2a)”仍然返回了false。这也是会什么HasSet的contains方法 返回false的原因,因为这个方法操作的是泛型,他调用的是一般化的Object上equals方法而非Point类上变化了的重载方法equals
*p*[qP:~5k051Testing软件测试网)z1u X?)mte一个更好但不完美的equals方法定义如下:
7O,^8oC/I ]0M"o:zH+_+h#](K0
@#^TE d'GT%K0// A better definition, but still not perfect 3{zB"ugd u5KY0@Override public boolean equals(Object other) {51Testing软件测试网 q']9e"_0Gn boolean result = false; 1XD6C-zJp;us]0 if (other instanceof Point) {51Testing软件测试网I G"L&^*Mp.G\ Point that = (Point) other; gD`LD8Dh.K%d.o0 result = (this.getX() == that.getX() && this.getY() == that.getY()); "t ~;}.h2d(O r*C)s0 }51Testing软件测试网 ODJh\ X%B#G2F5[ return result;51Testing软件测试网?*t,}+J'|4UOjU l } |
bK&z0s.M6j/|Z['AF0 现在equals有了正确的类型,它使用了一个Object类型的参数和一个返回布尔型的结果。这个方法的实现使用instanceof操作和做了一个造型。它首先检查这个对象是否是一个Point类,如果是,他就比较两个点的坐标并返回结果,否则返回false。51Testing软件测试网6g+t~S#^GV };jg,b
^3? P9^b#C9]*k0 陷阱2:重载了equals的但没有同时重载hashCode的方法
1]:`Z?,AKN0)B]wE k z N2h[@0 如果你使用上一个定义的Point类进行p1和p2a的反复比较,你都会得到你预期的true的结果。但是如果你将这个类对象放入到HashSet.contains()方法中测试,你就有可能仍然得到false的结果:51Testing软件测试网7|@dQe/w
p[k:EFBM051Testing软件测试网&\pR9qe+@
51Testing软件测试网-C$|i7a9_+EDY_E Point p1 = new Point(1, 2); HashSet<Point> coll = new HashSet<Point>();51Testing软件测试网(Rp;?[/L(fwT;?m5_ System.out.println(coll.contains(p2)); // 打印 false (有可能) S;u.|'Rw8N0 |
&H