1、覆盖equals时请遵守通用的约定
一、覆盖equals方法看起来似乎很简单,但是许多覆盖方式会导致错误,并且导致严重的后果,最容易避免这类问题的办法就是不覆盖equals方法,在这种情况下,类的每个实例都只于自身相等。如果满足以下任意一个条件,这就正是我们期望的结果:
a、类的每个实例本质上都是唯一的。(对于代表活动的实体而不是值的类来说确实如此,例如Thread。)
b、不关系类是否提供了“逻辑相等”的测试功能。
c、超类已经覆盖了equals,从超类继承过来的行为对于子类也是合适的。
d、类是私有的或是包级私有的,可以确定它的equals方法永远不会被调用。(在这种情况下,无疑是应该覆盖equals方法的,以防它被意外调用:
@Override publicl boolean equals(Object o) {
throw new AssertionError(); // Method is never called
}
)
二、如果类具有自己特有的“逻辑相等”,而且超类还没有覆盖equals以实现期望的行为,这时我们就需要覆盖equals方法。这通常是“值类”的情形。
有一中“值类”不需要覆盖equals方法,即用实例受控确保“每个值至多只存在一个对象”的类。枚举类型就属于这种类。
三、覆盖equlas时遵守的通用约定:
》自反性。对任何非null的引用值x,x.equals(x)必须返回true
》对称性。对任何非null的引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true。
》传递性。对任何非null的引用值x、y和z,如果x.equals(y)返回true,并且y.equals(z)也返回true,那么x.equals(z)也必须返回true。
》一致性。对任何非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)就会一致的返回true,或者一致的返回false。
》对任何非null的引用值x,x.equals(null)必须返回false。
四、写出高质量equals方法的诀窍:
a、使用==操作符检查“参数是否为这个对象的引用”。如果是,则返回true。这只不过是一种性能优化,如果比较操作符有可能很昂贵,就值得这么做。
b、使用instanceof操作符检查“参数是否为正确的类型”。
c、把参数转化成正确的类型。
d、对于该类中的每个“关键域”,检查参数中的域是否与该对象中对应的域相匹配。
对于即不是float也不是double类型的基本类型域,可以使用==操作符进行比较;对于对象域,可以递归调用equals方法;对于float域,可以使用Float.compare方法;对于double域,则使用Double.compare。如:Float中存在Float.NaN、0.0f。
有些对象引用域包含null可能是合法,为了避免NullPointException
(field == null ? o.field == null : field.equals(o.field))
如果field和o.field通常是相同的对象引用,下面会更快
(field == o.field || (field != null && field.equals(o.field)))
域的比较顺序可能会影响到equals方法的性能,为了获得最佳的性能,应该最先比较最有可能不一致的域,或者是开销最低的域,最理想的情况是两个条件同时满足的域。
e、当你编写完成了equals方法之后,应该问自己三个问题:它是否是对称的、传递的、一致的?
告诫:
a、覆盖equals时总要覆盖hashCode。
b、不要企图让equals方法过于只能。
c、不要equals声明中的Object对象替换为其他类型。
2、覆盖equals时总要覆盖hashCode
equals约定:
a、在应用程序的执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么多次对这同一个对象调用多次,hashCode方法都必须始终如一地返回同一个整数。
b、如果两个对象根据equals(Object)方法比较是相等的,那么调用这两个对象中任意一个对象的hashCode方法都必须产生相同的整数结果。
c、如果两个对象根据equals方法比较是不相等的,那么调用这两个对象中任意一个对象的hashCode方法,则不一定要产生不同的结果,但是程序员应该知道,给不相等的对象产生截然不同的结果,有可能提高散列的性能。
hashCode规则:
a、把某个非零的常量值,比如17,保存在一个名为result的int类型的变量中。
b、对于对象中的每个关键域f(指equals方法中涉及的每个域),完成一下步骤:
I.为该域计算int类型的散列码c:
i.如果该域是boolean类型,则计算(f ? 1 : 0)。
ii.如果该域是byte、char、short或者int类型,则计算(int)f。
iii.如果该域是long类型,则计算(int)(f ^ (f >>> 32))。
iv.如果该域是float类型,则计算Float.floatToIntBits(f)。
v.如果该域是double类型,则计算Double.doubleToLongBits(f)。然后按照步骤b.I.iii,为得到的long类型值计算散列值。
vi.如果该域是一个对象引用,并且该类的equals方法通过递归地调用equals的方法来比较这个域,则同样的为这个域递归地调用hashCode。如果需要更复杂的比较,则为这个域计算一个“范式”,然后针对这个范式调用hashCode。如果这个域为null,则返回0(或其他某个常数,但通常是0)。
vii.如果该域是一个数组,则把每一个元素当做单独的域来处理。如果数组域中的每个元素都很重要,可以利用jdk1.5中增加的Arrays.hashCode方法。
II.按照下面的公式,把步骤b.I中计算得到的散列码合并到result中:
result = 31 * result + c;
III.返回result。
3、始终要覆盖toString
默认的toString方法返回的是:类名 + @ + 散列码的无符号十六进制表示法。
toString通用约定:返回的字符串应该是一个“简洁的,但信息丰富,并且易于阅读的表达形式”,所有的子类都应该覆盖这个方法。
4、谨慎地覆盖clone
Cloneable接口的目的是作为对象的一个mixin接口,表明这样的对象允许克隆。
Clone方法的通用预定是非常弱的,下面是来自java.lang.Object规范中的约定内容:
创建和返回对象的一个拷贝,这个“拷贝”的精确含义取决于该对象的类。一般的含义是,对于任何对象x,表达式
x.clone() != x
将会是true,并且,表达式
x.clone().getClass() == x.getClass()
将会是true,但这都不是绝对的要求,虽然通常情况下,表达式
x.clone().equals(x)
将会是true,但是,这也不是一个绝对的要求。拷贝对象往往会导致创建它的类的一个新实例,但它同时也会要求拷贝内部的数据结构。这个过程没有调用构造器。
这个约定存在几个问题。“不调用构造器”的规定太强硬了。行为良好的clone方法可以调用构造器来创建对象,构造之后再复制内部数据。如果这个类是final的,clone甚至可能会返回一个由构造器创建的对象。
@Override public PhoneNumber clone(){
try{
return (PhoneNumber) super.clone();
} catch {
throw new AssertionError(); // Can't happen
}
}
clone方法返回的是PhoneNumber,而不是Object,从java1.5开始,这么做是合法的,在1.5中引入了协变返回类型作为泛型。
这里体现了一条通则:永远不要让客户去做任何类库能够替客户完成的事情。
如果对象中包含的域引用了可变的对象,使用上述这种简单的clone实现可能会导致灾难性的后果。例如:
public class Stack {
private Object[] elements;
}
假设你希望把这个类做成可克隆的。如果它的clone方法仅仅返回super.clone(),它的elements域将引用与原始Stack实例相同的数组。修改原始的实例会破坏克隆对象中的约束,反之亦然。
@Override public Stack clone(){
try{
Stack result = (Stack) super.clone();
result.elements = elements.clone();
return result;
} catch {
throw new AssertionError();
}
}
我们不一定要将element.clone()的结果转换成Object[]。自JAVA 1.5起,在数组上调用clone返回的数组,其编译时类型与被克隆数组的类型相同。还要注意,如果elements域如果是final的,上述方案就不能正常工作。
递归的调用clone有时还不够。
public class HashTable implements Cloneable {
private Entry[] buckets = ...;
private static class Entry {
final Object key;
Object value;
Entry next;
}
}
假如想Stack那样:
@Override public HashTable clone() {
try {
HashTable result = (HashTable) super.clone();
result.buckets = buckets.clone();
return result;
} catcht (CloneNotSupportedException e) {
throw new AssertionError();
}
}
虽然被克隆对象有他自己的散列数组,但是,这个数组引用的链表与原始对象是一样的,从而很容易引起克隆对象和原始对象中不确定的行为。为了修改这个问题,必须单独地拷贝并组成每个桶的链表。
public class HashTable implements Cloneable {
private Entry[] buckets = ...;
private static class Entry {
final Object key;
Object value;
Entry next;
Entry deepCopy(){
return new Entry(key, value, next == null ? null : next.deepCopy());
}
}
@Override public HashTable clone(){
try{
HashTable result = (HashTable) super.clone();
result.buckets = new Entry[buckets.length];
for(int i = 0; i < buckets.length; i++)
if (buckets[i] != null)
result.buckets[i] = buckets[i].deepCopy();
return reuslt;
}catch(CloneNotSupportedException) {
throw new AssertionError():
}
}
}
如果散列桶太长,为防止栈溢出,修改deepCopy
Entry deepCopy() {
Entry result = new Entry(key, value, next);
for (Entry p = result; p.next != null; p = p.next)
p.next = new Entry(p.next.key, p.next.value, p.next.next);
return result;
}
@Override public PhoneNumber clone(){
try{
return (PhoneNumber) super.clone();
} catch {
throw new AssertionError(); // Can't happen
}
}.
克隆复杂对象的最后一种办法是,先调用super.clone,然后把结果对象中所有域都设置成它们的空白状态,然后调用高层的方法来重新产生对象的状态。这种做法往往会产生一个简单、合理且相当优美的clone方法,但是它运行起来通常没有“直接操作对象及其克隆对象的内部状态的clone方法”快。
5、考虑实现Comparable接口
比较整数型基本类型的域,可以使用“>”和“<”,float、double使用compare方法。
如果一个类有多个关键域,那么,按什么样的顺序来比较这些域是非常关键的。你必须从最关键的域开始,逐步进行。
当用“-”判断结果是>0、<0、==0时,当心溢出。