你所忽略的,覆盖equals时需要注意的事项《effective java》

  我们都知道Object的equals的比较其实就是==的比较,其实是内存中的存放地址的比较。正常逻辑上:类的每个实例本质上都是唯一的。

  在工作中我们实际的业务逻辑往往有可能出现一些相对特殊的需求需要对equals方法进行重写,那么重写equals需要注意哪些规则或者通用的约定呢?

 

equals方法实现了等价关系(equivalence relation)

  • 自反性(reflexive)。对于任何非null的引用值xx.equals(x)必须返回true
  • 对称性(symmetric)。对于任何非null的引用值xy,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true
  • 传递性(transitive)。对于任何非null的引用值xyz。如果x.equals(y)返回true,并且y.equals(z)也返回true,那么x.equals(z)也必须返回true
  • 一致性(consistent)。对于任何非null的引用值xy,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(x)就会一致地返回true,或者一致的返回false
  • 对于任何非null的引用值xx.equals(null)必须返回false

第一点自反性不需要多说,基本上不可能会出现违背这条约定的情况当自己和自己比较的时候返回false。

第二点对称性,这种情况还是有可能会出现的。我们可以假设一个场景,这个场景是我们创建一个类并且里面只有一个String属性字段,这个类需要实现的是可以不区分字符串的大小写。

pubilc final class IgnoreCaseString {
    private final String s;

    public IgnoreCaseString(String s) {
        if (s == null)
            throw new NullPointerException();
        this.s = s;
    }

    @Override
    public boolean equals(Object o) {
        if (o instanceof IgnoreCaseString)
            return s.equalsIgnoreCase(
                    ((IgnoreCaseString) o).s);
        if (o instanceof String)
            return s.equalsIgnoreCase((String) o);
        return false;
    }

  ...//更多代码(重写equals就需要重写hashCode) }

在这个类中,equals方法的意图非常好,它的企图是可以与普通的字符串对象进行互操作。但是这段代码无意中触犯了对称性这个约定,从new IgnoreCaseString(“Po”).equals("po")是为true的,但是反过来“po”.equals(new IgnoreCaseString("Po"))的结果是false。假如违反了这一情况而没有去更正,他会破坏已有的集合框架的一些方法,使其变的不在准确。

IgnoreCaseString ics = new IgnoreCaseString("Po");
        List<IgnoreCaseString> list = new ArrayList<IgnoreCaseString>();
        list.add(ics);
        System.out.println(list.contains("po"));
        List<String> list1 = new ArrayList<>();
        list1.add("po");
        System.out.println(list1.contains(ics));

结果是,此时list.contains(s)会返回什么结果呢?没人知道,在Sun的当前实现中,它碰巧返回false,但这只是这个特定实现得出的结果而已。在其他的实现中,它有可能返回true(如上面代码中的list1.contains(ics)),或者抛出一个运行时(runtime)异常。一旦违反了equals约定,当其他对象面对你的对象时,你完全不知道这些对象的行为会这么样。

第三点传递性,equals约定:如果一个对象等于第二个对象,并且第二个对象又等于第三个对象,则第一个对象一定等于第三个对象。无意识违反这一情况其实不难想象,考虑子类的情形,子类增加的信息会影响到equals的比较结果。

public class TwoDCoordinate {
    private final int x;
    private final int y;
    public TwoDCoordinate(int x, int y) {
        this.x = x;
        this.y = y;
    }
    @Override
    public boolean equals(Object o) {
        if (!(o instanceof TwoDCoordinate))
        return false;
        TwoDCoordinate p = (TwoDCoordinate)o;
        return p.x == x && p.y == y;
    }
  ...//更多代码
(重写equals就需要重写hashCode)
} 
public class ThreeDCoordinate extends TwoDCoordinate{
  
private final int z;
  
public ThreeDCoordinate(int x, int y, int z) {
    
super(x, y); this.z=z;
  }

  @Override
  
public boolean equals(Object o) {
    
if (!(o instanceof TwoDCoordinate))
      
return false;
    
if (!(o instanceof ThreeDCoordinate))
      
return o.equals(this);
    
return super.equals(o) && this.z == ((ThreeDCoordinate) o).z;
  }
  
  ...//更多代码
(重写equals就需要重写hashCode)
}
ThreeDCoordinate t1 = new ThreeDCoordinate(1, 2, 3);
        TwoDCoordinate t2 = new TwoDCoordinate(1, 2);
        ThreeDCoordinate t3 = new ThreeDCoordinate(1, 2, 4);
        System.out.println(t1.equals(t2));
        System.out.println(t2.equals(t1));
        System.out.println(t2.equals(t3));
        System.out.println(t1.equals(t3));

结果是true  true   true   false,上述代码已经满足了自反性和对称性的约定,但是没有满足传递性,t1.equals(t2)为true,t2.equals(t3)为true,t1.equals(t3)却为false。

这种情况很多程序员会犯。

 想要避免这种情况,其实可以用getClass测试代替instanceof测试。如将TwoDCoordinate 的equals 方法改为

public boolean equals(Object o) {
        if (o == null || o.getClass() != this.getClass())
            return false;
        TwoDCoordinate p = (TwoDCoordinate)o;
        return p.x == x && p.y == y;
    }

这种替代方式其实不会太糟糕,但是结果却不会太理想,暂时没有想到令人满意的办法实现既可以扩展又不可实例化的类,但是可以考虑复合优先于继承。

第四点一致性,equals约定的第四个要求是,如果两个对象相等,它们就必须始终保持相等,除非它们中有一个对象(或者两个都)被修改了。换句话说,可变对象在不同的时候可以与不同的对象相等,而不可变对象则不会这样。当你在写一个类的时候,应该仔细考虑她是否应该是不可变的。如果认为它应该是不可变的,就必须保证equals方法满足这样的限制条件:相等的对象永远相等,不相等的对象永远不相等。

 

posted @ 2019-01-30 17:46  程序猿中的小白  阅读(314)  评论(0编辑  收藏  举报