Java中equals方法、==和hashCode的区别

时间:2021-01-01 16:09:03

最近对java中equals方法、“==”和hashCode方法的区别有点模糊,虽然以前看过不少博文,还是决定自己实际动手测试一下,整理一下以后有空看一下。

java中的数据类型,可分为两类:
1.基本数据类型,也称原始数据类型。byte,short,char,int,long,float,double,boolean,他们之间的比较,应用双等号(==),比较的是他们的值
2.复合数据类型(类)
  当他们用(==)进行比较的时候,比较的是他们在内存中的存放地址,除非是同一个new出来的对象,他们的比较后的结果为true,否则比较后结果为false。


JAVA当中所有的类都是继承于Object这个基类的,在Object中的基类中定义了一个equals的方法,其实现如下:

public boolean equals(Object anObject) { 
if (this == anObject)
return true;
}

由源码可以看到equals方法是直接使用“==”来实现的,所以这个方法的初始行为是比较对象的内存地址。


关于Objec类的equals方法的特点
A)自反性:X.equals(X)应该返回true
B)对称性:X.equals(y)为true,那么y.equals(X)也为true。
C)传递性:X.equals(y)为true,并且y.equals(Z)为true,那么X.equals(Z)也应该为true。
D)一致性:X.equals(y)的第一次调用为true,那么X.equals(y)的第二次、第三次、第n次调用也应该是true,如果在比较之间没有修改X和y。
E)对于非空引用X,X.equals(null)返回false。

但在一些类库当中equals方法被覆盖掉了,如String,Integer,Date在这些类重写了equals方法,重写的equals方法也要满足上面的5条原则。
比如String类中的equals方法实现如下:

public boolean equals(Object anObject) { 
if (this == anObject) {
return true;
}

if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = count;
if (n == anotherString.count) {
char v1[] = value;
char v2[] = anotherString.value;
int i = offset;
int j = anotherString.offset;
while (n-- != 0) {
if (v1[i++] != v2[j++])
return false;
}
return true;
}
}
return false;
}
由上面的源码我们发现String类型的数据在比较时先比较两个String对象的地址是否相同,如果地址相同直接返回true;如果地址不相同,则会继续比较String对象的值,如果值相等则返回true,值不相等则返回false。

总结如下:

对数据进行equals比较时:

如果没有覆写equals方法的情况下,他们之间的比较还是基于他们在内存中的存放位置的地址值的,因为Object的equals方法也是用双等号(==)进行比较的,所以比较后的结果跟双等号(==)的结果相同;

如果重写了equals方法的情况下,比如String、Integer、Date这些类,他们之间的比较是基于值的,值相同返回 true,否则返回false。

还有一些自己的定义的类,也可以重写equals方法(和hashCode方法),根据自己的需要来实现equals的逻辑。(参见本博文最后的程序实例,student类重写了这两个方法)

equals方法和“==”的区别暂时讲到这里


———————————华丽的分割线———————————


接下来说一下equals方法和hashCode方法的区别。

equals()反映的是对象或变量具体的值,即两个对象里面包含的值--可能是对象的引用,也可能是值类型的值

hashCode()是对象或变量通过哈希算法计算出的哈希值

之所以有hashCode方法,是因为在批量的对象比较中,hashCode要比equals来得快,很多集合都用到了hashCode,比如HashTable


首先equals()和hashcode()这两个方法都是从object类中继承过来的,

equals()是对两个对象的地址值进行的比较(即比较引用是否相同)。
hashCode()是一个本地方法,它的实现是根据本地机器相关的。

在object类中,hashCode定义如下:

public native int hashCode();
Object类中的hashCode方法比较的是对象的地址(引用地址)。

当然我们可以在自己写的类中覆盖hashcode()方法,比如String、Integer、Double等这些类都是覆盖了hashcode()方法的。

例如在String类中定义的hashcode()方法如下:

    public int hashCode() {  
int h = hash;
if (h == 0) {
int off = offset;
char val[] = value;
int len = count;

for (int i = 0; i < len; i++) {
h = 31 * h + val[off++];
}
hash = h;
}
return h;
}

hashCode()方法的特点:

A)在java的一次执行过程中,对于同一个对象的hashCode方法的多次调用,他们应该返回同样的值

B)对于两个对象来说,如果使用equals方法比较返回true,那么这两个对象的hashCode值一定是相同的

C)对于两个对象来说,如果使用equals方法比较返回false,那么这两个对象的hashCode值不要求一定不同,但是如果不同则可以提高应用的性能。

D) 对于Object类来说,不同的Object对象的hashCode值是不同的Object类的hashCode值表示对象的地址


以下几个问题一定要弄明白

1、为什么要重载equal方法?

因为Object的equals方法默认是两个对象的引用的比较,意思就是指向同一内存,地址则相等,否则不相等;

如果你现在需要利用对象里面的值来判断是否相等,则重载equals方法。

2、 为什么重载hashCode方法?

一般的地方不需要重载hashCode,只有当类需要放在HashTable、HashMap、HashSet等等hash结构的集合时才会重载hashCode

那么为什么要重载hashCode呢?就HashMap来说,好比HashMap就是一个大内存块,里面有很多小内存块,小内存块里面是一系列的对象,可以利用hashCode来查找小内存块hashCode%size(小内存块数量),所以当equal相等时,hashCode必须相等,而且如果是object对象,必须重载hashCode和equals方法。

3、 为什么equals()相等,hashCode就一定要相等,而hashCode相等,却不要求equals相等?

      1)因为是按照hashCode来访问小内存块,所以hashCode必须相等HashMap获取一个对象是比较key的hashCode相等和equals为true

      2)之所以hashCode相等,却可以equals不等,因为重写hashCode方法时hashCode的计算方式可以根据实际需要自定义的。

           比如ObjectA和ObjectB他们都有属性name,那么hashCode都以name计算,所以hashCode一样,但是两个对象属于不同类型,所以equals为false。

4、 为什么需要hashCode?

     1) 通过hashCode可以很快的查到小内存块。
     2) 通过hashCode比较比equals方法快,当get时先比较hashCode,如果hashCode不同,直接返回false。

equals方法和hashCode方法的区别暂时讲到这里


—————————————华丽的分割线———————————————————


下面举个例子综合说一下上面两个问题:

public class TestEquals {
    public static void main(String args[]) {
        Student stu1 = new Student("张一", 6);
        Student stu2 = new Student("张一", 6);
        
        if (stu1 == stu2)
        {
            System.out.println("stu1 == stu2");
        }else{
            System.out.println("stu1 != stu2");
        }
        
        if (stu1.equals(stu2)) {
            System.out.println("stu1 equals stu2");
            System.out.println("stu1的hashCode码:" + stu1.hashCode()
                             + "\nstu2的hashCode码:" + stu2.hashCode());
        } else {
            System.out.println("stu1 not equals stu2");
        }
        Student stu11 = new Student("张一", 6);    
        Student stu22 = stu11;
        
        if (stu11 == stu22)
        {
            System.out.println("stu11 == stu22");
        }else{
            System.out.println("stu11 != stu22");
        }
        
        if (stu1.equals(stu2)) {
            System.out.println("stu11 equals stu22");
        } else {
            System.out.println("stu11 not equals stu22");
        }
        
        String str1 = "我是小李";
        String str2 = "我是小李";
        
        if (str1 == str2){
            System.out.println("str1 == str2");
        }else{
            System.out.println("str1 != str2");
        }
        
        if (str1.equals(str2)){
            System.out.println("str1 equals str2");
        }else{
            System.out.println("str1 not equals str2");
        }
        
        String str11 = "我是小李";
        String str22 = new String("我是小李");
        
        if (str11 == str22){
            System.out.println("str11 == str22");
        }else{
            System.out.println("str11 != str22");
        }
        
        if (str11.equals(str22)){
            System.out.println("str11 equals str22");
        }else{
            System.out.println("str11 not equals str22");
        }
    
        String str111 = new String("我是小李");
        String str222 = new String("我是小李");
        
        if (str111 == str222){
            System.out.println("str111 == str222");
        }else{
            System.out.println("str111 != str222");
        }
        if (str111.equals(str222)){
            System.out.println("str111 equals str222");
        }else{
            System.out.println("str111 not equals str222");
        }
    }
}

class Student {
    private int age;
    private String name;
    
    public Student() {
    }

    public Student(String name, int age) {
        this.age = age;
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    //生成hashCode
    public int hashCode() {
        return (this.name.hashCode() + this.age) * 31;
    }
    
    //重写了equals方法,只要student对象中两个属性值相同就返回true
    public boolean equals(Object obj) {
        boolean result = false;
        if (obj == null) {
            result = false;
        }
        if (this == obj) {
            result = true;
        }

        if (obj instanceof Student) {
            Student stu = (Student) obj;
            if (stu.getName().equals(this.name) && stu.getAge() == (this.age)) {
                result = true;
            }

        } else {
            result = false;
        }
        return result;
    }
}

输出结果如下:

stu1 != stu2
stu1 equals stu2
stu1的hashCode码:24021466
stu2的hashCode码:24021466


stu11 == stu22
stu11 equals stu22


str1 == str2
str1 equals str2


str11 != str22
str11 equals str22


str111 != str222
str111 equals str222

分析:

Student类重写了equals方法和hashCode方法,equals方法比较的是student的name属性和age属性,两者都相等返回true,hashCode方法也是根据student的name属性和age属性计算出来的。

第一种情况:我们new了两个不同的student对象stu1和stu2,所以两者在内存中存放的地址是不一样的,所以stu1!=stu2,但是两个对象的属性值都一样,所以stu1.equals(stu2)=true,同时stu1和stu2的属性都一样,所以计算出来的hashCode也是一样的。在接下来的测试中(没有在代码中写出来,读者可自行测试)凡是equals为true的hashCode的都一样,印证了了对于两个对象来说,如果使用equals方法比较返回true,那么这两个对象的hashCode值一定是相同的

第二种情况:我们先new了一个stu11,然后让stu22指向了stu11,所以两者在内存中存放的地址是一样的,所以stu11==stu22,而且两个对象的属性值都一样,所以stu11.equals(stu22)=true。

第三种情况:这里涉及到字符串缓冲池的概念,String str1 = "我是小李";这句话创建了一个"我是小李"的String对象并放在字符串缓冲池里面;当使用 String str2 = "我是小李"; 这样的表达是创建字符串的时候,程序首先会在这个String缓冲池中寻找相同值的对象,由于str1先被放到了字符串缓冲池中,所以在str2被创建的时候,程序找到了具有相同值的 str1,str2引用str1所引用的对象“我是小李”。所以str1和str2引用的是同一个对象,所以str1 == str2,两者的内容也是一样的,所以str1 equals str2 == true。

第四种情况:String str11 = "我是小李";这句话创建了一个"我是小李"的String对象并放在字符串缓冲池里面;但是str22使用了 new 操作符,他明白的告诉程序:"我要一个新的!不要旧的!"于是一个新的"我是小李"Sting对象被创建在内存中。str11和str22的值相同,但是引用的对象不同即指向的内存地址不同,所以结果是str11!=str2,但是str11 equals str22 为true。

第五种情况:这种情况看起来和第三种情况很相似,但是实质上是不一样的,str111和str222都使用了new操作符,他们俩都重新创建了一个“我是小李”的String对象并引用它,所以是产生了两个存在内存不同位置的“我是小李”String对象,所以str111 !=  str222,但是str111 equals str222 为true。


接下来看一下重写hashCode函数前后的区别

实例1:

import java.util.HashSet;  
import java.util.Iterator;

public class HashSetTest {

public static void main(String[] args) {
HashSet hs = new HashSet();
hs.add(new Student(1, "小李"));
hs.add(new Student(2, "小张"));
hs.add(new Student(3, "小王"));
hs.add(new Student(1, "小李"));

Iterator it = hs.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
}
}

class Student {
int num;
String name;

Student(int num, String name) {
this.num = num;
this.name = name;
}

public String toString() {
return num + ":" + name;
}
}
输出的结果为:

    1:小李
2:小张
3:小王
1:小李
set集合有一个原则就是:集合中元素是无序的,但是不能重复,hashset继承自set也一样不能有重复元素。

但是上面的hashset添加了相等的元素,这是不是和hashset的原则违背了呢?答案是:没有。

为什么没有呢,是因为在根据hashcode()对两次建立的new Student(1,“小李”)对象进行比较时,生成的是不同的哈希码值,所以hashset把他当作不同的对象对待了,当然此时的equals()方法返回的值也不等。

为什么会生成不同的哈希码值呢?原因就在于我们自己定义的Student类并没有重写hashcode()和equals()方法,所以继承自object类中的hashcode()方法和equals方法仍然是比较引用对象的内存地址,两个student对象都是通过使用new方法创建的,两次生成的当然是不同的对象了,造成的结果就是两个对象的hashcode()返回的值不一样,所以Hashset会把它们当作不同的对象对待。

怎么解决这个问题呢?答案是:在Student类中重写hashcode()和equals()方法。重写hashcode()和equals()方法的Student类如下:

class Student {  
int num;
String name;

Student(int num, String name) {
this.num = num;
this.name = name;
}

public int hashCode() {//重写了Object类的hashCode方法
return num * name.hashCode();
}

public boolean equals(Object o) { //重写了Object类的equals方法
Student s = (Student) o;
return num == s.num && name.equals(s.name);
}

public String toString() {
return num + ":" + name;
}
}
输出的结果为:

1:小李
2:小张
3:小王
重写了equals方法和hashCode方法后,hashset集合中不存在重复元素了,根据重写的方法,即便两次调用了new Student(1,"小李"),我们在获得对象的哈希码时,两次new的student对象返回的哈希码肯定是一样的,当然根据equals()方法我们也可判断是相同的,所以在向hashset集合中添加时把它们当作重复元素看待了。

重写equals()和hashcode()小结:
      1)重点是equals,重写hashCode只是技术要求(为了提高效率)
      2)为什么要重写equals呢?因为在java的集合框架中,是通过equals来判断两个对象是否相等的
      3)在hibernate中,经常使用set集合来保存相关对象,而set集合是不允许重复的。在向HashSet集合中添加元素时,其实只要重写equals()这一条也可以。但当hashset中元素比较多时,或者是重写的equals()方法比较复杂时,我们只用equals()方法进行比较判断,效率也会非常低,所以引入了hashCode()这个方法,只是为了提高效率,且这是非常有必要的。


本篇博文就先到此为止吧,仓促之中写完第一篇技术博文,如果有错误请指正,相互交流相互进步……谢谢大家