如何写好 Java equals 方法

您所在的位置:网站首页 equal的名词形式怎么写 如何写好 Java equals 方法

如何写好 Java equals 方法

2024-07-17 13:05| 来源: 网络整理| 查看: 265

文章目录 前言操作符==equals 方法为什么要覆写equals方法equals 约定自反性对称性传递性一致性非空性 写好 equals 方法如何比较基本类型比较数组的比较引用类型比较 比较的性能写 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 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); } }

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

传递性

对于传递性,如果 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 方法,用到了本文所说的各种知识点。



【本文地址】


今日新闻


推荐新闻


    CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3