如何写好 Java equals 方法


前言

equals 方法是面试中一个常客,我以前面试的时候,被人问了一些比较刁钻的问题,但是我回答的并不是很好。这篇文章就对 equals 方法进行一个全方位的分析与总结。


操作符==

操作符 == 也是用于比较两个对象,如果比较的是基本类型,那么比较是对象的值,例如

int a = 10;
int b = 11;
System.out.println("a == b? = " + (a == b)); // false

如果比较的是引用类型,那么比较的是引用所指向的对象的地址,也就是说,比较两个引用是否指向相同的对象,例如

Hello h1 = new Hello();
Hello h2 = new Hello();
Hello h3 = h1;
System.out.println("h1 == h2? " + (h1 == h2)); // false
System.out.println("h1 == h3? " + (h1 == h3)); // true

equals 方法

equals 方法属于 Object 类,它默认是使用操作符==进行比较,如下

    public boolean equals(Object obj) {
        return (this == obj);
    }

也就是说,如果不覆盖 Object 类中的 equals 方法,那么 equals 方法默认就是比较引用是否指向相同的对象。


为什么要覆写equals方法

并不是所有时候都需要覆写equals方法,那么什么时候需要呢?我想,大部分情况是为了配合Java集合来使用。例如有下面一个引用类

public class Person {
	private String name;
	private int age;
	public Person(String name, int age) {
		this.name = Objects.requireNonNull(name);
		this.age = age;
	}
}

再往集合中添加一个 Person 对象,如下

List<Person> list = new ArrayList<>();
list.add(new Person("david", 11));

那么 list.contains(new Person("david",11)) 返回的却是 false,因为 ArrayList 的 contains 方法使用 equals 方法判断两个对象是否相等,而 Person 类没有覆盖这个方法,因此比较的是两个引用是否指向同一个对象,很显然,这不相等。

然而实际的情况中,我们希望两个 Person 对象,通过 equals 方法比较返回 true,这就是所谓的逻辑相等。因此,在需要的时候,我们有必须覆盖 equals 方法。


equals 约定

在 Object 类的源码中,我们可以看到 equals 方法的注释中,列举了覆写 equals 需要遵守的约定,如下

  1. 自反性 : 对于任何非空的引用值 x,x.equals(x) 应该返回 true。
  2. 对称性 : 对于任何非空的引用值 x 和 y,如果 x.equals(y) 返回 true,那么 y.equals(x)也应该返回 true。
  3. 传递性 : 对于任何非空引用值 x, y 和 z,如果 x.equals(y) 返回 true,并且 y.equals(z) 返回 true,那么 x.equals(z) 也应该返回 true。
  4. 一致性 : 对于任何非空应用值 x 和 y,如果在 equals 方法中所使用的对象的信息没有被修改,那么多次调用 x.equals(y) 必须返回相同的结果。
  5. 非空性 : 对于任何非空引用值 x,x.equals(null) 应该返回 false。

这些理论只是我们不需要去证明,只需要遵守即可。下面我们来分析下如何遵守这些预定,以及一些常见的范围约定的情况。

注意,如果覆盖了 equals 方法,还要记得覆盖 hashCode 方法。

自反性

对于自反性,我们很难想象 x.equals(x) 在什么情况下会返回 false,但是既然是自己比较自己,那么我们完全可以用操作符 == 来保证这一点

public class Person {
	String name;
	int age;
	
	public Person(String name, int age) {
		this.name = Objects.requireNonNull(name);
		this.age = age;
	}
	
	@Override
	public boolean equals(Object obj) {
		// 通过 == 保证自反性,同时也提升性能
		if (this == obj) {
			return true;
		}
	
		// 暂时省略其他逻辑
		
		return false;
	}
}

对于引用类型,操作符 == 是比较引用值的地址,因此可以很好的确保自反性,并且同时也提升了比较的性能,因为不用再去比较其他的内容。

对称性

对于对称性,如果 x.equals(y) 返回 true,那么 y.equals(x) 也应该返回 true。

一般来说,违背这个约定的有两种情况

  1. 在 equals 方法中把本类的对象与其他类对象进行比较。
  2. 通过继承,在子类中覆盖了超类的 equals 方法。

首先看第一种情况,在 equals 方法中与其他类对象进行比较

public class CaseInsensitiveString {
	private String s;
	
	public CaseInsensitiveString(String s) {
		this.s = Objects.requireNonNull(s);
	}
	
	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		
		if (obj instanceof CaseInsensitiveString) {
			return s.equalsIgnoreCase(((CaseInsensitiveString)obj).s);
		}
		
		// 与String类对象比较,违反对称性
		if (obj instanceof String) {
			return s.equalsIgnoreCase((String)s);
		}
		
		return false;
	}
}

在 equals 方法中,把 CaseInsensitiveString 对象与 String 对象进行比较,并认为可以相等。这会违反对称性,例如

CaseInsensitiveString s = new CaseInsensitiveString("Hello");
String s1 = "Hello";
System.out.println(s.equals(s1)); // true
System.out.println(s1.equals(s)); // false

CaseInsensitiveString 对象与 String 对象比较,返回 true,而 String 对象与 CaseInsensitiveString 对象比较返回 false,因此 CaseInsensitiveString 中的 equals 方法违反了对称性。

从这个例子可以看出,在 equals 方法中,应该只比较本类的对象,而不应该比较其他类的对象。

再来看第二种情况,子类覆盖了超类的 equals 方法。

超类的代码如下

public class Person {
	String name;
	int age;
	
	public Person(String name, int age) {
		this.name = Objects.requireNonNull(name);
		this.age = age;
	}
	
	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		if (obj instanceof Person) {
			Person p = (Person) obj;
			return name.equals(p.name) && age == p.age;
		}
		
		return false;
	}
}

子类代码如下

public class WeightPerson extends Person{
	private float weight;
	
	public WeightPerson(String name, int age, float weight) {
		super(name, age);
		this.weight = weight;
	}
	
	@Override
	public boolean equals(Object obj) {
		if (this == obj) return true;
		
		if (!(obj instanceof WeightPerson)) return false;
		
		WeightPerson wp = (WeightPerson) obj;
		
		return wp.weight == weight && super.equals(obj);
	}

}

WeightPerson 的 equals 方法比较了 weight ,然后通过超类的 equals 方法比较 name 和 age。这段代码看似很完美,但是它违反了对称性

Person p  = new Person("david", 11);
WeightPerson wp = new WeightPerson("david", 11, 60.0f);
System.out.println(p.equals(wp)); // true
System.out.println(wp.equals(p)); // false

从这段测试代码可以发现,超类 Person 对象与子类 WeightPerson 对象,互相比较的结果是不一致的,因此 WeightPerson 的 equals 方法违反了对称性。

其实这是面向对象语言中关于等价关系的一个基本问题: 我们无法在扩展可实例化的类的同时,既增加新的值组件(例如,成员变量), 同时又保留 equals 约定。

为了解决这个问题,有两种方法

  1. 让超类不可实例化,例如超类是一个抽象类。
  2. 选择组合而非继承。

其实,在子类中和超类的 equals 方法中,通过 getClass() 方法来判断类型,可以解决继承所造成的违反对称性的问题,例如

public class Person {
	String name;
	int age;
	
	public Person(String name, int age) {
		this.name = Objects.requireNonNull(name);
		this.age = age;
	}
	
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age &&
                name.equals(person.name);
    }
}

<< Effective Java >> 这一书中并不推崇这么做,而是推崇解决继承所造成的问题。

传递性

对于传递性,如果 x.equals(y) 返回 true, 并且 y.equals(z) 返回 true,那么 x.equals(z) 应该返回 true。

通常违反这个约定也是因为继承所造成的,但是只要我们正确的保证了反身性和对称性,传递性就可以保证了。

一致性

对于一致性,如果在 equals 方法中所使用的信息没有改变,那么多次调用 x.equals(y) 返回的结果应该一致。这是什么意思呢?

public class Person {
	String name;
	int age;
	
	public Person(String name, int age) {
		this.name = Objects.requireNonNull(name);
		this.age = age;
	}
	
	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		if (obj instanceof Person) {
			Person p = (Person) obj;
			return name.equals(p.name) && age == p.age;
		}
		
		return false;
	}
}	

Person 的 equals 方法使用了 name 和 age 进行比较,只要我们不修改这两个值,那么多次调用 x.equals(y) ,返回的结果永远是一致的。

因此,不要在 equals 方法中使用一些不可靠资源,例如主机名通过网络可以解析IP地址,但是我们不要比较这个IP地址,因为主机名可能不会变,但是IP地址可能会改变。

非空性

非空性说的是,x.equals(null) 应该返回 false,其实通过 obj instanceof Person 就可以保证非空性。


写好 equals 方法

现在总结下如何写好一个 equals 方法

  1. 通过操作符 == 进行比较,提升性能。
  2. 使用 instanceof 操作符检查类型,保证只与本类对象比较。
  3. 把参数转换为正确的类型。
  4. 对类中关键的信息进行比较。

例如下面的代码,就是一个高质量的 equals 写法

public class Person {
	String name;
	int age;
	
	public Person(String name, int age) {
		this.name = Objects.requireNonNull(name);
		this.age = age;
	}
	
	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		if (obj instanceof Person) {
			Person p = (Person) obj;
			return Objects.equals(name, p.name) && age == p.age;
		}
		
		return false;
	}
}	

但是,请注意,如果使用 instanceof 判断类型,那么注意规避面向对象语言中关于等价关系的一个基本问题: 我们无法在扩展可实例化的类的同时,既增加新的值组件(例如,成员变量), 同时又保留 equals 约定


如何比较

在 equals 方法中会比较关键信息,但是如何正确的比较,又是一个问题。这也是面试中进场会深究的一个问题。

基本类型比较

对于基本类型的比较,除了 float 和 double 类型外,其他基本类型使用操作符 == 进行比较。

对于 float 和 double 类型,最好分别使用 Float 和 Double 的静态 compare 方法,也就是 Float.compare(float, float)Double.compare(double, double) 进行比较。那么为什么不使用操作符 == 来比较这两个类型的值呢? 因为有以下两种异常

  1. 对于 float 类型,如果 x, y 的值都为 Float.NaN, x==y 返回的却是 false。 double 类型同样也有这样的问题。
  2. 对于 float 类型,如果 x, y 值为别为 +0.0f 和 -0.0f,x == y 返回 true, 但是如果把 float 对象转换为 Float 对象,然后使用 equals 方法进行比较,返回的却是 false 。double 类型也有同样的问题。

数组的比较

对于基本类型数组,可以使用对应类型 Arrays.equals() 进行比较,例如比较 double 数组,可以使用 Arrays.equals(double[], double[]) 进行比较。当然如果你不嫌麻烦,也可以遍历数组逐个进行比较。

引用类型比较

对于引用类型对象的比较,我们直接使用他们的 equals 方法进行比较,但是有一点需要注意,那就是空指针问题,例如

public class Person {
	String name;
	int age;
	
	public Person(String name, int age) {
		this.name = Objects.requireNonNull(name);
		this.age = age;
	}
	
	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		if (obj instanceof Person) {
			Person p = (Person) obj;
			return name.equals(p.name) && age == p.age;
		}
		
		return false;
	}
}

name 值可以为 null,因此 name.equals(p.name) 可以会产生空指针异常,我们可以有两种办法来避免这个问题

  1. 在构造函数中通过 Object.requireNonNull() 来保证参数不为空。
  2. 在 equals 方法中,通过 Objects.equals(Object, Object) 进行比较。

比较的性能

在 equals 方法中,如果我们注意一些细节,那么会提升方法的性能

  1. 先比较最有可能不一致的信息。
  2. 先比较开销较低的信息。
  3. 不比较那些可以推导出来的信息。
  4. 不比较一些无关的信息,例如用于同步的 Lock 对象。

写 equals 的科学方法

我们手动写代码会出错,但是机器很难犯错,我们可以利用 IDE 帮我们生成 equals 方法。

例如下面 equals 代码就是 IDE 生成的

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age &&
                Objects.equals(name, person.name);
    }

这个 IDE 生成的 equals 方法,用到了本文所说的各种知识点。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值