08-覆盖equals时请遵守通用约定
覆盖equals方法看起来似乎很简单,但是有许多覆盖方式会导致错误,并且后果非常严重。
类的每个实例本质上都是唯一的。对于代表活动实体而不是值的类来说确实如此,例如Thread.Object提供的equals实现对于这些类来说是正确的方式。
不关心类是否提供了逻辑相等的测试功能。例如,java.util.Random覆盖了equals,以检查两个Random实例是否产生相同的随机数序列,但是设计者并不认为客户端或者期望这样的功能。在这样的情况下,从Object继承得到的equals实现已经足够了。
超类已经覆盖了equals,从超类继承过来的行为对子类也是合适的。例如,大多数的Set实现都从AbstractSet继承equals实现,List实现从AbstractList继承equals实现,Map实现从AbstractMap继承equals实现。
类是私有的或是包及私有的,可以确定它的equals方法永远不会被调用。在这种情况下,无疑是应该覆盖equals方法的,以防它被以外调用:
什么时候应该覆盖Object.equals?
如果类具有自己的逻辑想的概念,而且超类还没有覆盖equals以实现期望的行为,这时我们就需要覆盖equals方法。这通常属于值类的情形。值类仅仅是一个表示值的类,例如Integer或者Date。程序员在利用equals方法来比较对象的引用时,希望知道它们在逻辑上是否相等,而不是想了解它们是否指向同一个对象。不仅必须覆盖equals方法,而且这样做使得这个类的实例可以被用做映射表(map)的键(key),或者集合(set)的元素,使映射或者集合表现出预期的行为。
有一种值类不需要覆盖equals方法,即用实例受控确保每个值至多只存在一个对象的类。枚举类型就属于这种类。对于这样的类而言,逻辑相同与对象等同是一回事,因此Object的equals方法等同于逻辑意义上的equals方法。
在覆盖equals方法时,必须遵守的通用约定
equals方法实现了等价关系(equivalence relation)
自反性(reflexive)。对于任何非null的引用值x,x.equals(x)必须返回true。
传递性(transitive)。对于任何非null的引用值x和y,当且仅当x.equals(y)返回true时,y.equals(z)也返回true,那么x.equals(z)也必须返回ture。
对称性(symmetric)。对于任何非null的引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true。
一致性(consistent)。对于任何非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)就会一致地返回true,或者一致地返回false。
对于任何非null的引用值x,x.equals(null)必须返回false。
自反性
假如违反了这一条,然后把类的实例添加到集合中,该集合的contains方法将果断地告诉你,该集合不包含你刚刚添加的实例。
对称性
任何两个对象对于它们是否相等的问题都必须保持一致。若无意违反此条,例如下面的类,它实现了一个区分大小写的字符串。字符串由toString保持,但在比较操作中被忽略。
public final class CaseInsensitiveString
{
private final 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((String)o);
}
return false;
}
}
在这个类中,equals方法的意图非常好,它企图与普通字符串对象进行互相操作。假设有一个不区分大小写的字符串和一个普通字符串:
CaseInsensitiveString cis=new CaseInsensitiveString("Polish");
String s="polish";
正如所料,cis.equals(s)返回true。问题在于,虽然CaseInsensitiveString类中的equals方法指导普通的字符串对象,但是,String类中的equals方法却并不知道不区分大小写的字符串。因此,s.equals(cis)返回false,显然违反了对称性。假设把不区分大小写的字符串对象放到一个集合中:
List<CaseInsensitiveString>list=new Arraylist<>();
list.add(cis);
此时返回的结果是不确定的,或者抛出一个运行时异常。一旦违反了equals约定,当其他对象面对你的对象时,你完全不知道这些对象的行为会怎么样。为了解决这个问题,使它变成一条单独的返回语句:
@override
public boolean equals(Object o)
{
((CaseInsenitiveString)o).s.equalsIgnoreCase(s);
}
传递性
如果一个对象等于第二个对象并且第二个对象又等于第三对象,则第一个对象一定等于第三对象。同样地,无意识地违反这条规定,考虑子类的情形,它将一个新的值组件添加到了超类中,换句话说,子列增加的信息会影响到equals的比较结果。
一致性
如果两个对象相等,它们就必须始终保持相等,除非它们中有一个对象被修改了。换句话说,可变对象在不同的时候可以与不同的对象相等,而不可变对象则不会这样。当你在写一个类的时候,应该考虑它是否应该是不可变的。如果认为是不可变的,就必须保证equals方法满足这样的限制条件:相等的对象永远相等,不相等的对象永远不相等。
无论类是否不可变的,都不要使equals方法依赖于不可靠的资源。例如,java.URL的equals方法依赖于URL总主机IP地址的比较。将一个主机名转变成IP地址肯需要访问网络,随着时间的推移,不确保会产生相同的结果。这样会导致URL的equals方法违反了equals约定,在实践中有可能引发一些问题。除了极少数的例外情况,equals方法都应该对驻留在内存中的对象执行确定性的计算。
非空性
指所有的对象都必须不等于null。
实现高质量equals方法
1.使用==操作符检查参数是否为这个对象的引用
2.使用instanceof操作符检查“参数是否为正确的类型”
3.把参数转换成正确的类型。因为转换之前进行过instanceof测试,所以确保会成功
4.对于类中的每个关键域,检查参数中的域和对象中对应的域相匹配。
如果第二步中的类型是接口,就必须通过接口方法访问参数中的域;如果该类型是类,取决于它们的可访问性
对于既不是float也不是double类型的基本数据类型域,可以使用==操作符进行比较;对于对象引用域,可以递归地调用equals方法;对于float域,可以使用Float.compare方法,double域同理。如果数组域中的每个元素都很重要,可以使用Arrays.equals
有些引用域包含合法的null,为了避免可能导致NullPointException异常,则使用如下的习惯用法来比较:
(field==null ? o.field==null : field.equals(o.field))
如果field和o.field通常是相同的对象引用,那么下面的做法会更快一些:
(field==o.field || (field!=null && field.equals(o.field)))
网友评论