关于Java中的equals方法

时间:2021-11-05 16:06:21

众所周知,Java中的equals方法是用来比较两个对象是否相等的。关于equalas方法,我们大概需要知道以下几点。

①Object类中的equals方法比较的是两个对象的地址

②八大基本数据类型包装类已经重写了equals方法,它们比较的是值

③String类也重写了equals方法

④关于null和equals方法

⑤关于自定义的类怎么重写equals方法

⑥重写equals方法时一般需要重写hashCode方法


①Object类中的equals方法比较的是两个对象的地址

这一点想必每个人都知道。而Object中的equals方法其实只有一行比较两个对象的地址是否相等的代码,用双等号来判断。如下:

return (this == obj);
反而是针对这一行代码所作的文档注释达到了惊人的46行。文档注释里包含了一些有趣的信息,一起来看一下。

    /**
* Indicates whether some other object is "equal to" this one.
* <p>
* The {@code equals} method implements an equivalence relation
* on non-null object references:
* <ul>
* <li>It is <i>reflexive</i>: for any non-null reference value
* {@code x}, {@code x.equals(x)} should return
* {@code true}.
* <li>It is <i>symmetric</i>: for any non-null reference values
* {@code x} and {@code y}, {@code x.equals(y)}
* should return {@code true} if and only if
* {@code y.equals(x)} returns {@code true}.
* <li>It is <i>transitive</i>: for any non-null reference values
* {@code x}, {@code y}, and {@code z}, if
* {@code x.equals(y)} returns {@code true} and
* {@code y.equals(z)} returns {@code true}, then
* {@code x.equals(z)} should return {@code true}.
* <li>It is <i>consistent</i>: for any non-null reference values
* {@code x} and {@code y}, multiple invocations of
* {@code x.equals(y)} consistently return {@code true}
* or consistently return {@code false}, provided no
* information used in {@code equals} comparisons on the
* objects is modified.
* <li>For any non-null reference value {@code x},
* {@code x.equals(null)} should return {@code false}.
* </ul>
* <p>
* The {@code equals} method for class {@code Object} implements
* the most discriminating possible equivalence relation on objects;
* that is, for any non-null reference values {@code x} and
* {@code y}, this method returns {@code true} if and only
* if {@code x} and {@code y} refer to the same object
* ({@code x == y} has the value {@code true}).
* <p>
* Note that it is generally necessary to override the {@code hashCode}
* method whenever this method is overridden, so as to maintain the
* general contract for the {@code hashCode} method, which states
* that equal objects must have equal hash codes.
*
* @param obj the reference object with which to compare.
* @return {@code true} if this object is the same as the obj
* argument; {@code false} otherwise.
* @see #hashCode()
* @see java.util.HashMap
*/

人工翻译一下。

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。

一致性。在equals实现代码不发生变化的情况下,多次调用equals方法返回值都一样。

另外,对于任意非空引用x,x.equals(null)返回false。

在Object类中,规定了一种最直接的判断两个对象是否相等的方法,即判断两个对象的引用值是否相等。

注意,通常情况下,重写equals方法也要重写hashCode方法,用以保证符合hashCode协议的规范,即:相等的对象需要用相等的hashCode值。

这里提到的关于equals方法的特征,不用刻意去记忆,因为大部分都是符合人们的自然逻辑的。至于其中讲到的关于和null的比较,以及关于hashCode的事将在后面说到。


②八大基本数据类型包装类已经重写了equals方法,它们比较的是值

很显然,通过双等号判断两个对象是否相等太苛刻了。更多的时候,我们需要通过比较两个对象的一些属性是否相等来判断这两个对象是否相等,即比较两个对象的值,而不是它们在内存中的地址来判定两个对象是否相等。相等和相同不是一个同一个概念。相同是完全一样,相同必然相等。相等只是值相等,相等未必相同。所以通常情况下,equals方法需要被重新。

开发人员根据具体业务创建的类Java是没法帮大家重写equals方法的,但是对于Java语言中固有的一些类,如八大基本数据类型包装类,它已经主动帮大家写好了实现。下面是Integer类中重写后的equals方法和测试代码。

源码:

    public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
测试代码:

package day171008;

public class TestEqualsOfInteger {

public static void main(String[] args) {
Integer i1 = new Integer(10);
Integer i2 = new Integer(10);
System.out.println(i1.equals(i2));//返回值为true
}
}


③String类也重写了equals方法

和八大基本数据类型包装类一样,作为Java语言固有的一种类,String也已经实现了equals方法的重写。代码如下:

    public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}

当然,从代码实现角度来说,由于和数值相比,字符串比较起来稍微复杂了一点,所以代码相对来说长了一点。基本的思路是,先比较两个对象的地址是否相等,若相等返回true,否则返回false。接下来通过instanceof关键字判断传递过来的对象是否是当前对象所属的类的实例,如果不是返回false,反之则继续比较。然后判断传递过来的对象,即String的长度和当前String的长度是否相等,如果不相等返回false,反之则继续比较。最后通过遍历的方式判断逐个比较两个String,即两个字符数组的各个下标位置的字符是否相等,若相等返回true,否则返回false。


④关于null和equals方法

其实在 ①Object类中的equals方法比较的是两个对象的地址 中已经提到过null的情况,即对于任意非空引用x,x.equals(null)返回false。

而我们需要注意的是,如果某个对象本身就是null,则调用equals方法让它和其它对象进行比较时则会报空指针异常(NullPointerException)。

这里有比较提一下空字符串和null的区别。如果一个对象的值是空字符串,表明该对象已经拥有了内存空间,只不是取值为空字符串。但是如果一个对象的值是null,表明只是声明了一个对象引用,它其实连内存空间都还没有。所以,对后者调用equals方法会导致空指针异常,而对前者来说则不会。

package day171008;

public class TestNull {

public static void main(String[] args) {
String str1 = "";
System.out.println(str1.equals(null));//返回false
String str2 = null;
System.out.println(str2.equals(null));//报NullPointerException
}
}


⑤关于自定义的类怎么重写equals方法

假如有以下Person类,它有两个字段name和age。对于两个对象而言,在name和age都相等的情况下,我们认为这两个对象相等(equals)。

package day171009;

public class Person {

private String name;
private int age;
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + age;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Person other = (Person) obj;
if (age != other.age)
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}

}
上面代码中已经写好的equals方法是通过eclipse自动生成的,我们可以自己动手DIY一个。

其实,对于传递过来的参数obj,无非有这么几种情况。一是和当前对象完全相同,即指向的是同一个对象。二是为null,直接就可以判定它和当前对象不相等。三是它和当前对象部分相等或完全不相等。四是它和当前对象完全相等。于是代码如下:

public boolean equals(Object obj) {
if(this == obj) {
return true;
}
if(obj instanceof Person) {
Person other = (Person)obj;
return other.name==this.name && other.age==this.age;
}
return false;
}


⑥重写equals方法时一般需要重写hashCode方法
关于hashCode是一个很大的坑,里面涉及到专业的算法。Java中的Object类中的hashCode方法是一个native方法,即通过调用其它编程语言的实现代码完成该功能。

这里我们只需要从逻辑上理解为什么重写equals方法时一般需要重写hashCode方法就可以了。

这个问题的根源在于,什么是hashCode,以及为什么需要hashCode。

hashCode一般用于set类型的集合中,用来快速判断要添加的元素是否已经在集合中存在。原本判断要添加的元素是否已经在集合中存在是可以通过equals方法来实现的,但是使用equals方法意味着要把要添加的元素和集合中的每一个元素对比一遍,在数据量很大的情况下,非常地费时费力。后来人们通过hash表来解决这个问题。即通过一定的算法(映射关系)为每个对象赋予一个恒定值,把该值和该对象的引用存储到哈希表里。这个恒定值是该对象(的引用)在哈希表中的位置,通过它可以非常方便地找到某个特定对象。而该哈希值往往是通过对该对象的字段进行计算的结果,这样,它就产生了这样一种效果。即相等的对象(通过equals进行判断的逻辑上的相等)必然拥有相等的哈希值,而不相等的对象的哈希值往往不同,但是也不一定,也有可能相同。反过来从哈希值的角度来说,哈希值相等的两个对象,可能相等,也可能不相等,但是哈希值不同的对象则肯定不相等。因此,当我们往set集合中添加一个新的元素时,就不必通过遍历的方式逐个比较equals的结果了,而是首先判断两个对象的哈希值是否相等,如果哈希值不相等,则可以断定要添加的元素和集合中已经存在的任意一个元素都不相等。反之,如果哈希值相等,则要添加的对象和集合中已经存在的某个对象可能相等,也可能不相等。然后拿这个哈希值去哈希表里找到对应的对象,通过equals方法进行对比就可以了。由于哈希值实质上是集合中的某个元素(对象)在哈希表中的位置,所以找到该对象是分分钟的事儿。这样就极大地提高了效率。

所以在set集合中进行存取的时候,首先通过hashCode方法判断哈希值是否相等,若不相等,返回false,若相等再通过hash值找到该对象,通过equals方法判断是否相等,若不相等返回false,若相等返回true。

回到最初的问题,为什么重写equals方法一般需要重写hashCode方法?为什么重写equals方法,一般是因为从Object类继承或者添加删除了某些字段,这种情况下,要想判断逻辑上的相等与否,就需要针对最新的字段信息作逻辑上的判断。同样,hashCode一般也是通过对对象的字段进行计算得出的。当字段被更新的情况下,有必要对hashCode的算法进行更新,以保证它能反映最新的字段的信息。否则,哈希值产生冲突的情况将会大大增加,这不是我们想看到的结果。