如何正确使用equals方法?

    equals方法的覆盖(Override)看起来很简单,但是许多的覆盖方式都是错误的,将导致非常严重的后果。规避这类后果的最简单的方法就是不覆盖equals方法,如果满足以下任一条件,就不需要覆盖equals方法:

  • 类的每一个实例本质上都是唯一的。对于大部分非值类(value class)的实例来说,Object类提供的equals实现都是正确的。
  • 不关心类是否提供了“逻辑相等(logical equality)”的比较功能
  • 父类已经覆盖了equals方法,且父类的equals实现对于子类也是适用的
  • 类是私有的或者是包级私有的,可以确定其equals方法永远不会被调用。在这种情况下,为了防止equals方法被意外调用,可以覆盖equals方法并抛出错误:
@Override
public boolean equals(Object o) {
    throw new AssertionError();
}

    equals方法的核心意义是实现了等价关系(equivalence relation),在覆盖equals方法时,必须遵守以下规范:

  • 非空性(non-nullity) 对于任何非null的引用值x,x.equals(null)必须返回false。
  • 自反性(reflexive) :对于任何非null的引用值x,x.equals(x)必须返回true。
  • 对称性(symmetric) :对于任何非null的引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true。
  • 传递性(transitive) :对于任何非null的引用值x、y和z,如果x.equals(y)返回true,且y.equals(z)也返回true,那么x.equals(z)也必须返回true。
  • 一致性(consistent) :对于任何非null的引用值x和y,只要equals方法所用到的对象的信息没有发生改变,多次调用x.equals(y)返回的结果就会保持一致。

    如果违反了以上五条规范,程序将在某些时候表现不正常,甚至崩溃,而且很难找到失败的根源。没有哪个类是孤立的,一个类的实例通常会被频繁地传递给另一个类的实例。有许多类,包括所有的集合类(collection class)在内,都依赖于传递给他们的对象是否遵守了equals的使用规范。

    下面,逐一讨论下这几条规范:

    非空性是指所有的对象都必须不等于null,一旦出现对象为空的情况,虽然很难出现o.equals(null)意外返回true的情况,但很可能意外抛出NullPointerException异常。许多类的equals方法通过显示的null判断来防止这种情况:

@Override
public boolean equals(Object o) {
    if (null == o)
        return false;
    ...
}

    这个判断是不必要的,在使用instanceof操作符检查参数类型时就已经实现了null判断功能:

@Override
public boolean equals(Object o) {
    if (! (o instanceof MyType))
        return false;
    MyType type = (MyType) o;
    ...
}

    自反性是指对象必须等于自身,这一条默认是自动满足的,除非强制进行错误的处理。加入违背了这一条,然后把该类的实例添加到集合类中,该集合的contains方法将查询不到添加的实例。

    对称性是指任何两个对象对于“它们是否相等”的判断都必须保持一致,违反这一条规定的情况使比较很容易出现的。例如,下面的类实现了一个不区分大小写的字符串。

public final class CaseInsensitiveString{
    private 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(((CaseInsensitiveString)o).s);
        if(o instanceof String)
            return s.equalsIgnoreCase((String)o);
        return false;
        ...
    }
}
    在这个类中,equals方法实现了与普通字符串对象进行互操作,假设有一个不区分大小写的字符串和一个普通字符串:

CaseInsensitiveString cis = new CaseInsensitiveString("Csdn");
String s = "csdn";
    这种情况下,cis.equals(s)将返回true,但是s.equals(cis)却返回false,这显然违背了对称性。这是因为String类中的equals方法并不知道CaseInsensitiveString。一旦违反了equals规范,当其他对象面对你的对象时,你完全不知道这些对象的行为会怎样。

    传递性是指如果一个对象等于第二个对象,并且第二个对象又等于第三个对象,则第一个对象一定等于第三个对象。当子类增加的信息会影响到equals的比较结果时,就容易违反这一规范,下面以Point类为例进行讲解:

public class Point{
    private int x;
    private int y;
    
    public Point(int x,int y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public boolean equals(Object o) {
        if(!(o instanceof Point))
            return false;
        Point p = (Point)o;
        return p.x == x && p.y ==y;
    } 
} 
    现在扩展Point类,添加颜色信息:

public class ColorPoint extends Point{
    private final Color color;
    
    public Point(int x,int y,Color color) {
        super(x,y);
        this.color = color;
    }

    @Override
    public boolean equals(Object o) {
    if(!(o instanceof Point))
        return false;
    if(!(o instanceof ColorPoint))
        return o.equals(this);
    return super.equals(o)&&((ColorPoint)o).color == color;
    } 
}
    假设,现在有如下三个对象:

ColorPoint p1 = new ColorPoint(1,1,Color.RED);
Point p2 = new Point(1,1);
ColorPoint p3 = new ColorPoint(1,1,Color.BLUE); 
    此时,p1.equals(p2)和p2.equals(p3)都返回true,但是p1.equals(p3)则是返回false,很显然违反了传递性。这是面向对象语言中关于等价问题的一个基本问题。我们无法在扩展(extends)可实例化的类的同时,既增加新的值组件(value component),同时又保留equals规范。虽然没有一种令人满意的办法可以既扩展可实例化的类,又增加值组件,但是还是有一种选择,即通过复合(composition)的方式实现(本文不再展开)。另外,可以在一个抽象类(abstract class)的子类中增加新的值组件,而不违反equals规范,也就是说,只要可能直接创建父类的实例,前面说的问题就不会发生。

    一致性是指对于可变对象,如果两个对象相等,他们就必须始终相等,除非它们中至少有一个对象被修改了。对于不可变对象,就必须保证equals方法满足这样的限制条件:相等的对象永远相等,不相等的对象永远不相等。无论类是否是不可变的,都不要使equals方法依赖于不可靠的资源。如果违反了这一条,要满足一致性的要求就十分困难了。除了极少数的例外情况,equals方法都应该对驻留在内存中的对象执行确定性的计算。

  以上,详细地讨论了equals方法的使用规范,现在梳理几条实现高质量equals方法的步骤和诀窍:

  1. 使用==操作符检查“参数是否为这个对象自身的引用”。
  2. 使用instanceof操作符检查“参数是否为正确的类型”。
  3. 把参数转换成正确的类型。
  4. 对于该类中的每个“关键”域,检查参数中的域是否与该对象中对应的域相匹配。
  5. 完成equals方法编写后,编写单元测试来检验equals方法是否满足对称性、传递性、一致性(自反性和非空性通常会自动满足)
    最后是几条覆盖equals方法时的注意点:
  • 覆盖equals方法时总要覆盖hashCode方法;
  • 不要企图让equals方法过于智能;
  • 不要将equals方法声明中的Object对象替换为其他的类型:这样是重载(Overload)了Object.equals方法,并没有实现equals方法的覆盖,增加了代码的复杂性。
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在Java中,Object类中定义了hashCode()和equals()方法。hashCode()方法返回对象的散列码,equals()方法比较两个对象是否相等。如果你自定义了一个类,并且要在该类的实例之间进行比较和查找,那么你需要覆盖hashCode()和equals()方法。 重写hashCode()方法的步骤如下: 1. 声明一个整型变量result并初始化为一个非零的奇数。 2. 对于对象的每个关键域f,计算该域的哈希码c: a. 如果该域是基本类型,则计算Type.hashCode(f),其中Type是相应基本类型的包装类。 b. 如果该域是一个对象引用,并且该类的equals方法通过递归调用equals的方式比较这个域,则同样递归调用这个域的hashCode()方法。如果这个域的值为null,则返回0。 c. 如果该域是一个数组,则需要对数组的每个元素进行上述操作,可以使用Arrays.hashCode方法。 3. 将计算得到的哈希码c合并到result中,可以使用result = 31 * result + c的方式。 4. 返回result。 重写equals()方法的步骤如下: 1. 首先判断传入的对象是否与当前对象引用相同,如果是则返回true。 2. 判断传入的对象是否为null或者与当前对象的类不同,如果是则返回false。 3. 将传入对象转换为当前类的类型。 4. 对于对象的每个关键域f,检查该域在传入对象和当前对象中的值是否相等。如果所有的关键域都相等,则返回true;否则返回false。 注意:当你重写equals方法时,也应该重写hashCode方法,以便在将对象放入哈希表等数据结构中时能够正确地工作。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值