如何在Java中避免equals方法的隐藏陷阱(上)

上一篇 / 下一篇  2012-09-18 14:25:51 / 个人分类:Java

51Testing软件测试网 U.Qox+zI!N

  译者注:你可能会觉得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)QvY6cZ X

  除非你忘记了面向对象抽象的好处,否则在当你继承一个新类或在类中增加了一个值组件时你无法同时保证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-`0

D7MB"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:Pr0

b#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);
wOI N dh\7}-y0hashSet.contains(elem2);    // returns false!
51Testing软件测试网0@2? ^E!A:_|-N

  当equals重载时,这里有4个会引发equals行为不一致的常见陷阱:51Testing软件测试网?y V;Tg o bd7V

51Testing软件测试网3jD+R9DC1sD

  1、定义了错误的equals方法签名(signature)Defining equals with the wrong signature.

.T f:}]G#l9fn*SB8U0^051Testing软件测试网.OUd P6p't ]

  2、重载了equals的但没有同时重载hashCode的方法。Changing equals without also changing hashCode.

)` @^;bS.x4@2n0

M`@(}e }P8x0  3、建立在会变化字域上的equals定义。Defining equals in terms of mutable fields.

:i W0e2W{'`(V%V051Testing软件测试网n$])[#}4`l

  4、不满足等价关系的equals错误定义Failing to define equals as an equivalence relation.

Yc ]:N*XG[051Testing软件测试网"y(| wg,e,` XY

  在剩下的章节中我们将依次讨论这4中陷阱。

rQ S)Jt051Testing软件测试网*Nd.Q%X#Rd8f'u

  陷阱1:定义错误equals方法签名(signature)

5M9z ruK#W:y@j{0

2d1_5x t7r:C1P0  考虑为下面这个简单类Point增加一个等价性方法:

%N.F}C'}a jc0

\!HN R%i(j%o2l0public class Point {

z Oe TX(Z9l0

|z4m%WLWm0    private final int x;51Testing软件测试网*]]0n6QSo9B
    private final int y;

;iy OYS LD]051Testing软件测试网1IdA,b6L5YW F

    public Point(int x, int y) {
zZ['Av0];}$iuo0        this.x = x;51Testing软件测试网Zb"trGEr
        this.y = y;51Testing软件测试网7W_'HFe6l H1hsf/w&A
    }
51Testing软件测试网V7KZ Ic,u.p&H

51Testing软件测试网~x%a.pLB4_l~

    public int getX() {51Testing软件测试网C ?'U)I/o*sO9e6y
        return x;
y-u9p{)~W Xa1UC D#q0    }
51Testing软件测试网X/EQ$]4r

H `W.f@I1Y0    public int getY() {
\%bVF^}A$W1i4F+e0        return y;51Testing软件测试网-y\$t @:K
    }
51Testing软件测试网.g X"Aa)IQ8uU3B

i%i9Y-Y/~^7\Xo;i0    // ...51Testing软件测试网:M%g;`X"@$NqN*C]
}

G7I$Rzs x0
51Testing软件测试网r4Q*sR$?UT

  看上去非常明显,但是按照这种方式来定义equals就是错误的。

D!V kiSS0// An utterly wrong definition of equals51Testing软件测试网a;ZD~;a%f
public boolean equals(Point other) {
e"dndehLI0  return (this.getX() == other.getX() && this.getY() == other.getY());51Testing软件测试网zCCq8xG.N
}
%u5J:_s"O0
51Testing软件测试网4g_ OS]

  这个方法有什么问题呢?初看起来,它工作的非常完美:51Testing软件测试网e2]#LYV*B6D:L

51Testing软件测试网 W8mLZ&[9O.W'I

51Testing软件测试网|&LZ-q)Z0rM#BX L

,j"C$}@ vyN0Point p1 = new Point(1, 2);
8iSE Y?B'H\P0Point p2 = new Point(1, 2);

Xcvcx051Testing软件测试网_eXlM/X7\)t(X/j7~R

Point q = new Point(2, 3);51Testing软件测试网\p b EV~']l

51Testing软件测试网q C.Th%Or]X

System.out.println(p1.equals(p2)); // prints true51Testing软件测试网,T,\pW*b0@&{;d8{

'o,Z q5tz!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`t p O

*|SE a'Px"x@0

9B8X"ZW$LG^P0import java.util.HashSet;

7Ak d&}%O-n\051Testing软件测试网|!e^.Q k)pFq;z

HashSet<Point> coll = new HashSet<Point>();51Testing软件测试网8qb'[n v3{M'a
coll.add(p1);
51Testing软件测试网 ^2K9C+ej Y QM

51Testing软件测试网v&Tf$S6[ e ~

System.out.println(coll.contains(p2)); // prints false51Testing软件测试网R vF w N7}2Y3L

51Testing软件测试网"w2@!im M2m!Ip8kB9c

  为什么coll中没有包含p2呢?甚至是p1也被加到集合里面,p1和p2是是等价的对象吗?在下面的程序中,我们可以找到其中的一些原因,定义p2a是一个指向p2的对象,但是p2a的类型是Object而非Point类型:

+F,r%U9g1K9V#}(P0

R2y3H+NWl0

DJ_ Y\UKs*B;L'_0
Object p2a = p2;

Fo A"w$l;IXy7H0  现在我们重复第一个比较,但是不再使用p2而是p2a,我们将会得到如下的结果:51Testing软件测试网i7?[Zw+t @

51Testing软件测试网-q(e){'M6t D!M9x

51Testing软件测试网;F%wtu*B U L4r$e

System.out.println(p1.equals(p2a)); // prints false
51Testing软件测试网^5eI,s$N

  到底是那里出了了问题?事实上,之前所给出的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]0

M"o:zH+_+h#](K0

@#^TE d'GT%K0
// A better definition, but still not perfect
3{zB"ugdu5KY0@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&^*M p.G\
        Point that = (Point) other;
gD`LD8D h.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 };j g,b

^3? P9^b#C9]*k0  陷阱2:重载了equals的但没有同时重载hashCode的方法

1]:`Z?,AKN0

)B] wE kz 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);
,vY'dO+K(u0Point p2 = new Point(1, 2);
51Testing软件测试网$~6x0y~%sq

51Testing软件测试网k(hW!dM:VBp

HashSet<Point> coll = new HashSet<Point>();51Testing软件测试网(Rp;? [/L(fwT;?m5_
coll.add(p1);
51Testing软件测试网 R{1]0e&{ Bq6I"f

51Testing软件测试网 |cXx Hv#[0^e2G.Q

System.out.println(coll.contains(p2)); // 打印 false (有可能)

S;u.|'Rw8N0

&Hkh/P&L^U0  事实上,这个个结果不是100%的false,你也可能有返回ture的经历。如果你得到的结果是true的话,那么你试试其他的坐标值,最终你一定会得到一个在集合中不包含的结果。导致这个结果的原因是Point重载了equals却没有重载hashCode。51Testing软件测试网6{~mN#o&{;g$c$Z

51Testing软件测试网)m#M.|x1z

  注意上面例子的的容器是一个HashSet,这就意味着容器中的元素根据他们的哈希码被被放入到”哈希桶 hash buckets”中。contains方法首先根据哈希码在哈希桶中查找,然后让桶中的所有元素和所给的参数进行比较。现在,虽然最后一个Point类的 版本重定义了equals方法,但是它并没有同时重定义hashCode。因此,hashCode仍然是Object类的那个版本,即:所分配对象的一个 地址的变换。所以p1和p2的哈希码理所当然的不同了,甚至是即时这两个点的坐标完全相同。不同的哈希码导致他们具有极高的可能性被放入到集合中不同的哈 希桶中。contains方法将会去找p2的哈希码对应哈希桶中的匹配元素。但是大多数情况下,p1一定是在另外一个桶中,因此,p2永远找不到p1进行 匹配。当然p2和p2也可能偶尔会被放入到一个桶中,在这种情况下,contains的结果就为true了。

z6eqJk051Testing软件测试网#eT(E[ co;T[D jzp3y

  最新一个Point类实现的问题是,它的实现违背了作为Object类的定义的hashCode的语义。

9e H}ZKc ](\)wc0

s(}~A FVS0  如果两个对象根据equals(Object)方法是相等的,那么在这两个对象上调用hashCode方法应该产生同样的值。51Testing软件测试网:Fb3N8S] A

SZun&iUVc0  事实上,在Java中,hashCode和equals需要一起被重定义是众所周知的。此外,hashCode只可以依赖于equals依赖的域来产生值。对于Point这个类来说,下面的的hashCode定义是一个非常合适的定义。51Testing软件测试网!I t/]?A-^

51Testing软件测试网f4f'~]"k&t/p7a}"_V

5zOzAT F(@6gq0
51Testing软件测试网1_#{~0zA'Q0KK Gd.H3x

public class Point {51Testing软件测试网6cT @ I.q7^L1|4v

51Testing软件测试网 [xc$w`~

    private final int x;51Testing软件测试网 VD/G1nFAO7n#q
    private final int y;

8X q T'\P(m0

m7?!nE8~h!q0    public Point(int x, int y) {51Testing软件测试网?ny \Dc g
        this.x = x;51Testing软件测试网`5cY)^bk`
        this.y = y;
7p H6F,H:q0    }

'i-_:j*O'~,@3KwC!ZR0

-`#R/P z.v Y|S/g0    public int getX() {51Testing软件测试网*[)_z"y{"f
        return x;
1S1g{9n(^Y [0    }
51Testing软件测试网g0Z2i+Y!zf7j

`Q3T}Md(}0    public int getY() {
6\L9X!C t-w`C0        return y;51Testing软件测试网 drN8g5] `)d;X7d/a
    }

@"jh@q xm+Q051Testing软件测试网z%O JI h}#SC/T

    @Override public boolean equals(Object other) {51Testing软件测试网@[cN([`,k/]
        boolean result = false;
)Eq%r0@"xr%Sgc;u0        if (other instanceof Point) {
1Y K B:G2]X)mBc0            Point that = (Point) other;
6b;Mu(P@0            result = (this.getX() == that.getX() && this.getY() == that.getY());51Testing软件测试网Q!D F:M+n%B/z#F)\
        }
|`(Y'W(T0        return result;51Testing软件测试网;uAxf_E{4b"a
    }

c{}D RKq;Tf0

2??!~0eK,L^0    @Override public int hashCode() {51Testing软件测试网 f-E*J9[&B N aR@;q'e(j1R
        return (41 * (41 + getX()) + getY());
Y$P3V7{|(i0    }

?U,oh,uX.m051Testing软件测试网ULgu/\I*xw

}51Testing软件测试网(tFm h8`

w ])}r,y"S0  这只是hashCode一个可能的实现。x域加上常量41后的结果再乘与41并将结果在加上y域的值。这样做就可以以低成本的运行时间和低成本代码大小得到一个哈希码的合理的分布(译者注:性价比相对较高的做法)。

c0r"^9Lv)k5y2d051Testing软件测试网}U*AS5Z$zh,n

  增加hashCode方法重载修正了定义类似Point类等价性的问题。然而,关于类的等价性仍然有其他的问题点待发现。

8b6Jt~(G9r051Testing软件测试网o1AT,|;CD%rq

  未完待续......51Testing软件测试网']An(FA$^.]$`a\

51Testing软件测试网 q4n8D} C%}

TAG:

 

评分:0

我来说两句

Open Toolbar