谨慎重置equals方法
在Java中equals方法是一个很重要的方法,也是一个相对容易出错的地方。本片文章主要以实例来重点讲述如何实现,已经在实现中所要注意的几个问题。希望能够读者带来一些启发,减小出错的概率。
在介绍equals方法实现之前,我们有所必要了解一些基本的概念。Equals方法顾名思义,就是比较两个对象是否是等价。它必须遵守五个条件。
1)反身性(reflexive)。简单的说就是如果有一个非空对象A,那么必定存在如下关系: A.equals( A )返回一定是true。
2)对称性(symmetric)。比如存在两个非空对象A与B。如果A.equals( B )返回true,那么必定B.equals( A )也返回true。
3)传递性(transitive)。比如存在三个非空对象A,B,C。如果A.equals( B )返回true且B.equals( C )返回true,那么A.equals( C )也一定是返回true。
4)一致性(consistent)。对于存在的非空对象A与B。A.equals( B )在无论什么外在条件下,都应该一直返回true或者false。
5)非空性。对于非空对象A,在任何条件下,A.equals( null )一直返回false。
在正常的情况下,equals方法的实现应用了等于方法,也就是说,对于对象A与B,只有并且仅仅只有A与B是同一个对象才是等价。这个是默认的Object类的实现方法。在有些时候,我们要求equals方法是等价方法,而非等于方法,这时候equals方法应该被重置。在Java定义的Object对象中,其实equals方法原本的意义就是等价方法,而非等于,这点一定要记住,不要被表面的英语词汇所迷惑。个人感觉,可能equals方面这个名字起得不够好,equivalence之类的名字可能更加的容易理解。千万要记住一点:equals方面是等价方法而非等于方法!此外我们还可以改注意,在重置equals方法的时候,一定也要重置类中的hashCode方法。因为如果两个对象等价,那么它的hashCode方法返回的值必定要相同。当然没有硬性规定,不同对象的hashCode方法返回的值一定不相等。我个人建议如果两个对象不相等,hashCode返回的值最好也不相等。因为如果这样,那么当使用Hashtable的时候,效率提高是显然的。难道不是吗?!J
读者在阅多了上面相关信息后,应该对equals方面和其相关的东西有了一个基本的认识。下面我讲讲述如何来正确的时相equals方法。
1)Point1D的定义
public class Point1D { protected int x;
public Point1D( int x ) { this.x = x; }
public boolean equals( Object o ) { if ( !(o instanceof Point1D) ) return false;
return x == ((Point1D) o).x; }
public int hashCode() { return super.hashCode() * 31 + x; } } |
2)Point2D的定义
public class Point2D extends Point1D { protected int y;
public Point2D( int x, int y ) { super( x ); this.y = y; }
public boolean equals( Object o ) { if ( !(o instanceof Point2D) ) return false;
Point2D p2d = (Point2D) o;
if ( this == p2d ) return true;
return y == p2d.y && super.equals( p2d ); }
public int hashCode() { return super.hashCode() * 31 + y; } } |
你觉得有问题吗?嗯…,看上去好像没有问题,定义的挺好的,真的吗?!让我们测试一下。
public class Test { public static void main( String[] args ) { Point1D p1d = new Point1D( 10 ); Point2D p2d = new Point2D( 10, 20 );
if ( p1d.equals( p2d ) ) System.out.println( "P1D is equal to P2D" ); else System.out.println( "P1D is unequal to P2D" );
if ( p2d.equals( p1d ) ) System.out.println( "P2D is equal to P1D" ); else System.out.println( "P2D is unequal to P1D" ); } } Result: P1D is equal to P2D P2D is unequal to P1D |
WOW,为什么,竟然是这样!很明显原实现没有对子父类进行有效的检查,而仅仅对其他类做了相关的检测,一个大bug!其实这个是所有面向对象语言中都存在的问题,我们可以参考请谨慎实现operator==操作符函数。它用了一个所有面向对象中对通用的方法解决了它,那么是否我们这里也要这么做呢?嗯…,我觉得完全可以,不过在Java中我们是否可以发现更好的方法呢?噢,我想到了,为什么不检查类的class来完成对应的检查工作呢?它很好的完成了我们要得工作,难道不是吗?!让我们动手吧J!
3)Piont1D的重定义
public class Point1D { protected int x;
public Point1D( int x ) { this.x = x; }
public boolean equals( Object o ) { if ( o == null ) return false;
if ( !getClass().equals( o.getClass() ) ) return false;
return x == ((Point1D) o).x; }
public int hashCode() { return super.hashCode() * 31 + x; } } |
4)Point2D的重定义
public class Point2D extends Point1D { protected int y;
public Point2D( int x, int y ) { super( x ); this.y = y; }
public boolean equals( Object o ) { if ( o == null ) return false;
if ( !getClass().equals( o.getClass() ) ) return false;
Point2D p2d = (Point2D) o;
if ( this == p2d ) return true;
return y == p2d.y && super.equals( p2d ); }
public int hashCode() { return super.hashCode() * 31 + y; } } |
现在他们能够正确工作了吗?让我们再试试看。
public class Test { public static void main( String[] args ) { Point1D p1d = new Point1D( 10 ); Point1D p2d = new Point2D( 10, 20 ); Point1D p2d2 = new Point2D( 10, 20 ); Point1D p2d3 = new Point2D( 10, 20 ); Point1D p2d4 = new Point2D( 10, 30 ); Point1D p2d5 = new Point2D( 10, 40 ); Point1D p2d6 = new Point2D( 10, 50 );
// Check reflexive if ( p1d.equals( p1d ) ) System.out.println( "Reflexive/t/t-/tpassed!" ); else System.out.println( "Reflexive/t/t-/tfailed!" );
// Check symmetric if ( ((p1d.equals( p2d ) ? 1 : 0) ^ (p2d.equals( p1d ) ? 1 : 0)) == 0 ) System.out.println( "Symmetric/t/t1/tpassed!" ); else System.out.println( "Symmetric/t/t1/tfailed!" );
if ( ((p2d.equals( p2d2 ) ? 1 : 0) ^ (p2d2.equals( p2d ) ? 1 : 0)) == 0 ) System.out.println( "Symmetric/t/t2/tpassed!" ); else System.out.println( "Symmetric/t/t2/tfailed!" );
// Check transitive if ( p2d.equals( p2d2 ) && p2d2.equals( p2d3 ) ) { if ( p2d.equals( p2d3 ) ) System.out.println( "Transitive/t/t1/tpassed!" ); else System.out.println( "Transitive/t/t1/tfailed!" ); }
if ( !p2d4.equals( p2d5 ) && !p2d5.equals( p2d6 ) ) { if ( !p2d4.equals( p2d6 ) ) System.out.println( "Transitive/t/t2/tpassed!" ); else System.out.println( "Transitive/t/t2/tfailed!" ); }
// Check null if ( !p1d.equals( null ) && !p2d.equals( null ) ) System.out.println( "Null/t/t/t-/tpassed!" ); else System.out.println( "Null/t/t/t-/tfailed!" ); } } Result: Reflexive - passed! Symmetric 1 passed! Symmetric 2 passed! Transitive 1 passed! Transitive 2 passed! Null - passed! |
Great! 都很好,都通过了!比较请谨慎实现operator==操作符函数中的方法,我们是否真的已经到了很满意的程度了呢?!好像还有一些瑕疵要修补吧。比如说,我们要对Point2D实例个数进行统计,所以就添加了两个静态的属性和一个静态方法。
public class StatPoint2D extends Point2D { protected static Object lock = new Object(); protected static long p2dCounter = 0;
static synchronized long getCounter() { return p2dCounter; }
public StatPoint2D( int x, int y ) { super( x, y ); synchronized ( lock ) { ++ p2dCounter; } }
public static void main( String[] args ) { Point2D p2d = new Point2D( 10, 20 ); Point2D p2d2 = new StatPoint2D( 10, 20 );
if ( p2d.equals( p2d2 ) ) System.out.println( "P2D is equal to P2D2" ); else System.out.println( "P2D2 is unequal to P2D" ); } } Result: P2D2 is unequal to P2D |
从等价概念而言,P2D和P2D2是等价的,但是我们运行的结果和我们的预料出现的偏移。如何解决呢?问题的关键是我们使用了getClass来作为判断依据,它用的并非是真正的等价依据。所以我们最好的方法是使用等价Class来替换掉它(因为getClass是final修饰的,我们不能够通过重置来完成它)。我们在Point1D中加入如下方法。
protected Object getEquivalentClass() { return getClass(); } |
5)Point1D再次重新定义
public class Point1D { protected int x;
protected Object getEquivalentClass() { return getClass(); }
public Point1D( int x ) { this.x = x; }
public boolean equals( Object o ) { if ( !(o instanceof Point1D) ) return false;
Point1D p1d = (Point1D) o;
if ( !getEquivalentClass().equals( p1d.getEquivalentClass() ) ) return false;
return x == p1d.x; }
public int hashCode() { return super.hashCode() * 31 + x; } } |
6)Point2D再次重新定义
public class Point2D extends Point1D { protected int y;
public Point2D( int x, int y ) { super( x ); this.y = y; }
public boolean equals( Object o ) { if ( o instanceof Point2D ) { Point2D p2d = (Point2D) o;
if ( this == p2d ) return true;
if ( y != p2d.y ) return false; }
return super.equals( o ); }
public int hashCode() { return super.hashCode() * 31 + y; } } |
7)StatPoint2D再次重新定义
public class StatPoint2D extends Point2D { protected static Object lock = new Object(); protected static long p2dCounter = 0;
static synchronized long getCounter() { return p2dCounter; }
protected Object getEquivalentClass() { return Point2D.class; }
public StatPoint2D( int x, int y ) { super( x, y ); synchronized ( lock ) { ++ p2dCounter; } }
public static void main( String[] args ) { Point2D p2d = new Point2D( 10, 20 ); Point2D p2d2 = new StatPoint2D( 10, 20 );
if ( p2d.equals( p2d2 ) ) System.out.println( "P2D is equal to P2D2" ); else System.out.println( "P2D2 is unequal to P2D" ); } } Result: P2D is equal to P2D2 |
非常酷,用一个简单的getEquivalentClass的替代函数完成了灵活的类型操作。这个应该算是比较全面的实现了,你说呢?!