在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的替代函数完成了灵活的类型操作。这个应该算是比较全面的实现了,你说呢?!