第七条 :在改写equals的时候请遵守通用约定
如果不改写equals方法,则每个实例只与它自己相等。我们同时也期望这样的情况:
1. 一个类的每个实例本质上都是惟一的。对于代表了活动实体而不是值的类,比如Thread。
2. 不关心一个类是否提供了“逻辑相等”的测试功能。Random不需要比较两个随机数是否相等。
3. 超类已经改写了equals,从超类继承过来的行为对于子类也是合适的。
4. 一个类是私有的,或者是包级私有的,并且可以确定它的equals方法永远也不会被调用。这样的情况下,应该要改写equals,防止有一天它被调用到:
public boolean equals(Object o){
throw new UnsupportedOperationException();
}
当一个类有自己特有的“逻辑相等”概念(不同于对象身份的概念),而且超类也没有改写equals以实现期望的行为,这时我们需要改写equals方法。这通常适合于“值类value class”的情形。
有一种值类可以不要求改写equals方法,即类型安全枚举类型typesafe enum(21条)。因为安全枚举类型保证每个值至多只存在一个对象,所以对这样的情况而言,Object的实例相等等同于逻辑意义上的相等。
改写equals方法必须遵守通用约定,来自java.lang.Object的规范:
1.自反性(reflexive)。x.equals(x)一定为true;
2.对称性(symmetric)。当且仅当y.equals(x)返回true时,x.equals(y)返回true;
3.传递性(transitive)。如果x.equals(y)返回true,并y.equals(z)也返回true,则x.equals(z)也返回true。
要想在扩展一个可实例化的类的同时,既要增加新的特性,同时还要保留equals约定,没有一个简单的办法可以做到这点。可考虑符合优先于继承(第14条)。
Java平台中,一些类是可实例化的子类,并加入了新特征,例如Java.sql.Timestamp对java.util.Date进行子类化,增加了nanoseconds域。如果Timestamp和Date对象被用于同一个集合中,或者以其他方式被混合在一起,则会出现不正确的行为。
注意,你可以在一个抽象类的子类中增加新的特性,而不会违反equals的约定。对于用层次来代替联合而得到的一种类层次结构非常重要。只要不可能创建超类的实例,那么前面所述的种种问题都不会发生。
4.一致性(consistent)。多次调用x.equals(y),返回一致的值。
5.对于任意的非空引用值x,x.equals(null)一定返回fals。
即不允许对象为空的情况抛出NullPointerException异常。但也不需要单独对null进行检查,可以使用instanceof操作符:
public boolean equals(Object o){
if(!o instanceof MyType))
return false;
…
}
如果不检查类型,则会抛出ClassCastException异常,这违反了equals约定。但如果instanceof的第一个操作符是null的话,则会返回false。
实现高质量的equals方法:
1. 使用==操作符检查“实参是否为指向对象的一个引用”。这时一种性能优化。
2. 使用instanceof操作符检查“实参是否为正确的类型”。
3. 把实参转换到正确的类型。
4. 对于该类中每一个“关键”域,检查实参中的域于当前对象中对应的域是否匹配。
如果第二步中的类型是个接口,需要通过接口的方法,访问实参中的关键域。对于非float和double类型的原语类型域,可用==比较;对象引用域,可递归地调用equals方法;对floa域,可使用Float.floatToIntBits转换成int类型的值,然后用==比较int类型的值;对double域,可使用Double.doubleToLongBits转换成long类型的值,然后用==比较long类型的值;(考虑到Float.NaN、-0.0f以及类似的double类型的常量);对于数组域,把以上的原则应用到每个元素上。对于某些对象引用域包含null是合法的:
(field==null ? o.field == null : field.equals(o.field))
如果field和o.field通常是相同的对象引用,可用:
(field == o.field || (field != null && field.equals(o.field)))
对于某些类,针对某个域的比较操作较复杂,应该在该类的规范上明确加以说明。如果这样,在每一种对象内部保存一个“范式”,这样equals方法可以根据范式进行低开销的精确比较。这项技术对非可变类最合适。
域的比较顺序会影响equals方法的性能。所以最先比较最有可能不一致的域或者比较开销最低的域。一般不需要比较冗余域,但如果一个冗余域代表了这整个对象的一个概括描述,则当最终比较结果为false时,可省下比较实际数据所需的开销。
5. 当你编写完成了equal方法之后,应该问自己:是否对称、传递、一致的?(其他两个通常会自行满足)。
一些告诫:
a. 当改写equals的时候,总要改写hashCode。
b. 不要企图让equals方法过于聪明。
c. 不要使equals方法依赖于不可靠的资源。
d. 不要将equals声明中的Object对象替换为其他的类型。因为这样并没有改写equals(Object o),而是重载了它。