Java学习————equals方法详解

0 阅读4分钟

一、引言

在大家学习Java的过程中,equals方法是我们非常常见的方法之一,也一定遇到过equals()和==的区别这个老生常谈的问题。在我们使用Java编写程序时,需要重写equals方法我们往往采用自动生成或者使用Lombok的@Data注解来生成的方式来重写equals方法,因此我们对其了解可能并不是很深入,同时在前面分享HashSet的文章中,提出了为什么hashCode方法和equals方法必须一起重写的问题,对于这些问题我们在此进行探讨。

二、equals方法详解

- equals方法和==的区别

我们都知道==比较的是地址,而equals方法比较的是内容,但是注意如果是我们自己定义的类,如果不重写equals方法它和==一样比较的是两个对象的地址。这是因为在Object类中,equals方法就是利用了==来进行比较。

image.png 而我们在使用equals方法的场景是重写后的equals方法,比较其内容,例如String类的equals方法底层是这样比较字符串内容的:

重写equals方法的原则

  1. 自反性(Reflexive)x.equals (x) = true对象必须等于它自己。
  2. 对称性(Symmetric)x.equals (y) == y.equals (x)不能我认识你,你不认识我。
  3. 传递性(Transitive)x.equals (y) = true,y.equals (z) = true则 x.equals (z) = true保证逻辑的连贯性。
  4. 一致性(Consistent)多次调用结果相同只要对象没变,结果就不变。
  5. 非空性(Non-null)x.equals (null) = false任何非空对象与 null 都不等。

关于instanceof陷阱

在 equals 方法中用 instanceof 判断类型,会破坏对称性。我们知道instanceof方法 是Java 二元运算符,作用:判断左边的对象实例,是不是右边这个类(包括它自身、所有父类、实现的接口)的实例,返回 boolean 布尔值。在我们重写equals方法时,为了判断传入的对象和被比较对象是否属于同一个类时会利用instanceof()方法进行判断,这存在一定的问题,举个例子:

class Point {
    private final int x;
    private final int y;
 
    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
 
    // 错误写法:使用 instanceof
    @Override
    public boolean equals(Object o) {
        // 这里允许所有子类都通过判断
        if (!(o instanceof Point)) return false;
        Point p = (Point) o;
        return x == p.x && y == p.y;
    }
}
 
// 子类:带颜色的点
class ColorPoint extends Point {
    private final String color;
 
    public ColorPoint(int x, int y, String color) {
        super(x, y);
        this.color = color;
    }
}
 
// 测试类
public class Test {
    public static void main(String[] args) {
        Point p = new Point(1, 1);
        ColorPoint cp = new ColorPoint(1, 1, "RED");
 
        // 父类.equals(子类) → true
        System.out.println(p.equals(cp));  // true
 
        // 子类.equals(父类) → false
        System.out.println(cp.equals(p));  // false
    }
}

显然上面的例子中equals方法违背了对称性,那我们怎么避免这个问题呢?答案是不用 instanceof,改用 getClass () 判断类型完全一致。

@Override
public boolean equals(Object o) {
    // 必须是同一个类
    if (o == null || getClass() != o.getClass())
        return false;
    Point point = (Point) o;
    return x == point.x && y == point.y;
}

三、为什么equals方法必须和hashCode方法一起重写

像HashMap、HashSet这类哈希结构集合,存储和查找元素时,首先会根据对象的hashCode()哈希值确定元素存放的数组位置。只有当两个对象哈希值相同,出现哈希碰撞时,集合会继续调用equals()方法,比较对象内容是否真正相等,如果我们不重写equals方法,比较的是对象地址,就会将我们业务上认为完全相同的对象存入集合中,进而引发各种问题。

如果我们只重写了equals(),没有重写hashCode():假设对象u1作为HashMap的键存入集合,根据业务逻辑u1.equals(u2) = true,说明两个对象内容相等,使用u2理应能取出u1对应的值。但由于没有重写hashCode(),会沿用Object类原生方法,两个对象哈希值不同。HashMap查找元素时,先通过哈希值定位桶位置,u2会定位到和u1完全不同的数组下标,根本不会进入u1所在的桶,也就不会执行后续的equals()比较,最终导致无法查到对应数据,还会出现元素重复存储的问题。