Effective Java 对于所用对象都通用的方法 8.覆盖equals时请遵守通用约定.txt

时间:2021-09-23 16:03:20
对于eclipse覆盖equals方法就是Alt+Shift+S,而AS就是Alt+Insert。覆盖很简单,可是却容易导致错误,而且后果很严重。最容易避免的方法就是不覆盖,这样类就只与他自身相等。


如果满足了以下任何一个条件,那么覆盖equals就能达到期望了。


1、类的每个实例本质上都是唯一的。对于代表活动实体而不是值(value)的类来说确实如此,例如Thread。Object提供的equals实现对于这些类来说是正确的行为。
2、不关心类是否提供了“逻辑相等(logical equality)”的测试功能。如java.util.Random覆盖了equals,以检查两个Random实例是否产生相同的随机序列,但是设计者并不认为客户需要或者期望这样的功能。在这样的情况下,从Object继承得到的equals实现已经足够了。
3、超类已经覆盖了equals,从超类继承过来的行为对于子类来说也是合适的。
4、类是私有的或是包级私有的,可以确定它的equals方法永远不会被调用。在这种条件下,无疑应该覆盖equals方法,以防止它被意外调用。


@Override  
public boolean equals(Object c) {  
    throw new AssertionError(); // Method is never called  
}  


上面的代码,只是对equals的一种废弃,并没有新加什么功能。




那么,什么时候应该覆盖Object.equals呢?如果类具有自己特有的“逻辑相等”的概念(不同于对象等同的概念),而且超类还没有覆盖equals以实现期望的行为,这时我们就需要覆盖equals方法。这通常是属于“值类(value class)”的情形,值类仅仅是一个表示值的类,例如Integer或者Date。我们在利用equals方法来比较值对象的引用时,希望知道它们是逻辑上是否相等,而不是想了解它们是否指向同一个对象。为了满足需求,不仅必须覆盖equals方法,而且这样做也使得这个类的实例可以被用做映射表的键或者是集合的元素,使映射或者集合表现出预期的行为。


有一种"值类"是不需要覆盖equals方法的,即用实例受控确保“每个值至多只存在一个对象的类”,枚举就属于这种类,对于这样的类而言,逻辑相同与对象相同同一回事,因此Object的equals方法等同于逻辑意义上的equsls方法。


在覆盖equals方法时,必要要遵守他的通用约定。下面约定的内容,来自object的规划(JDK1.6)。


equals方法实现了等价关系:


1、自反性(reflexive)。对于任何非null的引用值x,x.equals(x)必须返回true。
      对象必须等于自身,如果违反了这一规定,当你把该类的实例添加到集合中,该集合的contains方法将不会认为它你刚刚存在集合的实例在集合中。
 
2、对称性(symmetric)。对于任何非null引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true。
      任何两个对象对于“它们是否相等”的问题都必须保持一致,考虑下面的类,它实现了一个区分大小写的字符串。字符串由toString保存,但是在比较操作中被忽略。
 
 // Broken - violates symmetry!  
public final class CaseInsensitiveString {  
    private final String s;  
      
    public CaseInsensitiveString(String s) {  
        if (s == null)  
            throw new NullPointerException();  
        this.s = s;  
    }  
      
    // Broken - violates symmetry!  
    @Override  
    public boolean equals(Object o) {  
        if (o instanceof CaseInsensitiveString) {  
            return s.equalsIgnoreCase(((CaseInsensitiveString) o).s);  
        }  
        if (o instanceof String) { // One-way interoperability!  
            return s.equalsIgnoreCase((String) o);  
        }  
        return flase;  
    }  
}  
这个类中,equals方法的意图非常好,它还实现了与普通字符串对象进行操作。假设我们有一个不区分大小写的字符串和一个普通的字符串

CaseInsensitiveString cis = new CaseInsensitiveString("Yancy");  
String s = "yancy"  
    正如所料,cis.equals(s)返回为true,问题在于,虽然CaseInsensitiveString类中的equals方法知道普通的字符串对象(String),但是String类中的equals方法却并不知道不区分大小写。因此s.equals(cis)返回false。显然违反了对称性,假设把不区分大小写的字符串对象放到一个集合中。


List<CaseInsensitiveString> list = new ArrayList<CaseInsensitiveString>();  
list.add(cis);  

此时list.contains(s)会返回一个不确定结果
    一旦违反了equals约定,当其他对象面对你的对象时,你完成不知道这些对象的行为会怎么样。
这了解决这问题,只需把企图与String互操作的这段代码从equals有一方看去掉就可以了。这样做之后,就可以重构该方法,让它变成一条单独的返回语句。


@Override  
public boolean equals(Object o) {  
return (o instanceof CaseInsensitiveString) &&  ((CaseInsensitiveString) o).s.equalsIgnoreCase(s);  
}  
    
3、传递性(transitive)。对于任何非null引用值x、y、z,如果x.equals(y)返回true,并且y.equals(z)也返回true,那么x.equals(z)也必须返回true。
      
如果一个对象等于第二的对象,并且第二个对象又等于第三个对象,则第一个对象一定等于第三个对象。


4、一致性(consistent)。对于任何非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)就会一致的返回true或false。
    如果两个对象相等,它们就必须始终保持相等,除非它们中有一个对象或者俩个都被修改了。或者说,可变对象在不同的时候可以与不同的对象相等,而不可变对象则不会这样。
当我们在写一个类的时候,如果它是不可变的,那就必须保证equals方法满足这样的限制条件:相等的对象永远相等,不相等的对象永远不相等。
       
        
5、非空性(not null)。对于任何非null的引用值x,x.equals(null)必须返回false。
    尽管很难想象什么情况下o.equals(null)调用会意外地返回true,但是意外抛出NullPointerException异常的情况倒不难想象。通用约定不允许抛出NullPointerException异常。许多类的equals方法都通过一个显式的null测试来防止这种情况:


public boolean equals(Object o) {  
if (o == null)  
return false;   
}  
    这项测试是不必要的。为了测试其参数的等同性,equals方法必须先把参数转换成适当的类型,以便可以调用它的访问方法,或者访问它的域。在进行转换之前,equals方法必须使用instanceof操作符,检查其参数的正确类型:


public boolean equals(Object o) {  
if (!(o instanceof MyType))  
return false;  
MyType mt = (MyType) o;  
}  
    如果漏掉了这一步的检查,并且传递给equals方法的参数又是错误的类型,那么equals方法将会抛出ClassCaseException,这就违反了equals的约定,但是,如果指定instanceof的第一个操作数为null,那么,不管第二个操作数是哪种类型,instanceof操作符都返回false。因此不再需要独立的null检查。
        
综上所述,得出以下实现高质量equals方法的诀窍。
    1、使用==操作符检查”参数是否为这个对象的引用“。如果是,就返回true,这只不过是一种性能优化,如果比较操作很昂贵时就值得这么做。

    2、使用instanceof操作符检查”参数是否为正确的类型“。一般来说,所谓正确的类型指的是equals方法所在的那个类。

    3、把参数转换成正确的类型。
    4、对于该类中的每个”关键“域,检查参数中的哉是否与该对象中对应域相匹配。
    5、当编写完成equals方法之后,必须考虑是否是对称的,传递的,一致的。