文章目录
前言
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 需要遵守的约定,如下
- 自反性 : 对于任何非空的引用值 x,x.equals(x) 应该返回 true。
- 对称性 : 对于任何非空的引用值 x 和 y,如果 x.equals(y) 返回 true,那么 y.equals(x)也应该返回 true。
- 传递性 : 对于任何非空引用值 x, y 和 z,如果 x.equals(y) 返回 true,并且 y.equals(z) 返回 true,那么 x.equals(z) 也应该返回 true。
- 一致性 : 对于任何非空应用值 x 和 y,如果在 equals 方法中所使用的对象的信息没有被修改,那么多次调用 x.equals(y) 必须返回相同的结果。
- 非空性 : 对于任何非空引用值 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。
一般来说,违背这个约定的有两种情况
- 在 equals 方法中把本类的对象与其他类对象进行比较。
- 通过继承,在子类中覆盖了超类的 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 约定。
为了解决这个问题,有两种方法
- 让超类不可实例化,例如超类是一个抽象类。
- 选择组合而非继承。
其实,在子类中和超类的 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 方法
- 通过操作符 == 进行比较,提升性能。
- 使用 instanceof 操作符检查类型,保证只与本类对象比较。
- 把参数转换为正确的类型。
- 对类中关键的信息进行比较。
例如下面的代码,就是一个高质量的 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)
进行比较。那么为什么不使用操作符 == 来比较这两个类型的值呢? 因为有以下两种异常
- 对于 float 类型,如果 x, y 的值都为 Float.NaN, x==y 返回的却是 false。 double 类型同样也有这样的问题。
- 对于 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)
可以会产生空指针异常,我们可以有两种办法来避免这个问题
- 在构造函数中通过
Object.requireNonNull()
来保证参数不为空。 - 在 equals 方法中,通过
Objects.equals(Object, Object)
进行比较。
比较的性能
在 equals 方法中,如果我们注意一些细节,那么会提升方法的性能
- 先比较最有可能不一致的信息。
- 先比较开销较低的信息。
- 不比较那些可以推导出来的信息。
- 不比较一些无关的信息,例如用于同步的 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 方法,用到了本文所说的各种知识点。