Java的equals()方法和hasahCode()方法详解


Java的equals()方法和hasahCode()方法详解


目录

简介

Object是Java所有类的超类,equals和hashCode方法都是从Object中来的,讨论equals和hashCode方法必定绕不开Object、继承、重写

正文

什么是equals方法

Object类中equals方法用来比较两个对象的属性是否相等,也可以说是比较两个引用对象是否为同一个对象

因为在Object中没有属性,所以就只比较了两个引用指向的对象是否相等,只要对象不相等,那么就返回false

这样有些绝对,其他类默认直接使用Object的equals方法,但是这是没法直接拿来用的

//Object类中对应的源码:
public class Object {
	public boolean equals(Object obj) {
		return (this == obj);
	}
}

如果两个new的对象比较直接用equals方法,无论内容多么相等,只要不是同一个对象,equals都会返回false

比如下面的例子:

public class Main {
    public static void main(String[] args) {
        Students student1 = new Students(20);
        Students student2 = new Students(20);
        System.out.println(student1.equals(student2));
        //输出结果为false
    }

}
class Students{
    private int age;
    public Students (int a){
         a = this.age;
    }
}

所以我们常用的、比较引用类型的值是否相同的->equals方法主要 是 根据逻辑重写后的equals方法,下面我们通过equals方法的重写来详细理解equals方法的具体实现逻辑:

想要实现“两个Students类中age元素相同,他们的equals为true”,equals方法修改后:

public class Main {
    public static void main(String[] args) {
        Students student1 = new Students(20);
        Students student2 = new Students(20);
        System.out.println(student1.equals(student2));
        //输出结果为true
    }

}
class Students{
    private int age;
    public Students (int a){
        this.age = a;
    }
    @Override
    public boolean equals(Object o){
        if(this == o) return true;
        //比较其属性值
        Students student = (Students) o;
        return age == student.age;
    }
}

但是上面的代码很容易出现空指针异常类型转换的异常

因为在Students类中重写的equals方法中,在**Students student =(Students)o;**(强转)的时候没有对参数 o 进行检查

需要检查两个地方

1. **首先确保参数 o 不能为null**

2. **其次保证参数 o 是Students的类或者子类** *(父类无法使用子类的特有属性,强转父类到子类也会报错)*
public class Main {
    public static void main(String[] args) {
        Students student1 = new Students(20);
        Students student2 = new Students(20);
        System.out.println(student1.equals(student2));
    }

}
class Students{
    private int age;
    public Students (int a){
        this.age = a;
    }
    @Override
    public boolean equals(Object o){
        if(this == o) return true;
        //instanceof的用法是 A instanceof B,用来判断A是否为B类或者B的子类
        if (!(o instanceof Students)) return false;
        Students student = (Students) o;
        return age == student.age;
    }
}

上面用到了instanceof来判断,这样就可以防止空指针和转换异常的出现

简单的equals方法就分析结束了

所以equals判断的内容总结下来就是三步

  1. 判断两个引用指向的对象是否相等
  2. 判断传来的参数是否为当前类或者当前类的子类
  3. 比较各个属性值是否相等

但是如果属性是对象的引用,那第三步该怎么比呢?那不就套娃了嘛

public class Test8 {
    public static void main(String[] args) {
        Students student1 = new Students(20,"张三");
        Students student2 = new Students(20,"张三");
        System.out.println(student1.equals(student2));
    }

}
class Students{
    private int age;
    private String name;
    public Students (int a){
        this.age = a;
    }
    public Students (int a,String b) {this.age = a; this.name = b;}
    @Override
    public boolean equals(Object o){
        if(this == o) return true;
        if (!(o instanceof Students)) return false;
        Students student = (Students) o;
        //增加了name == student.name	
        return age == student.age && name.equals(student.name);
    }
}

题外话:在此处有个疑问:equals方法中是怎么访问到student对象的私有变量name的?同理,age属性也是私有的,不是说私有的变量需要使用public的set、get方法来操作吗…

套娃就套娃,套到哪个引用类型就用它的equals方法,这样也不用担心有的地方没有判断到或者会判断失误,上面对String引用类型使用了String的equals方法,下面我们可以看一下String重写equals方法的源码:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    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;
    }
}

首先对比引用类型的地址是否相同,即是否为同一个String类型的对象,若相同返回true,否则对比String对象的内容是否相同,若相同返回true,否则返回false;

仅仅对于上述代码来说,仍然可能会有空指针的异常,即name为null,无法调用equals方法,所以应该对name进行空指针判断,刚好Objects工具类内置的equals方法可以在比较两个对象的同时加上null判断

Objects.equals源码:

public final class Objects {
    public static boolean equals(Object a, Object b) {
        return (a == b) || (a != null && a.equals(b));
    }
}

所以代码最后一行

return age == student.age &&  name.equals(student.name);

改为

return age == student.age && Objects.equals(name,student.name);

但是这就结束了吗,非也非也,如果这是子类之间的比较,那么继承来的父类的属性值并没有做比较,所以在比较的时候就会出现比较结果不对的情况(参考博客lombok——@EqualsAndHashCode(callSuper = true)注解的使用)

所以继续改:

@Override
public boolean equals(Object o){
    if(this == o) return true;
    if (!(o instanceof Students)) return false;
    //仅限于有父类且父类有equals方法的情况,否则就调用到超类Object的equals方法,刚刚我们看过它的源码,知道它简陋的不靠谱,所以一定要注意这一点:有父类才加这一句话(我们这里Students没有父类,所以先不加)
    //为什么放在第三行???
    //if(!super.equals(o)) return false;
    Students student = (Students) o;
    return age == student.age && Objects.equals(name,student.name);
}

为什么加这一句?

为什么放在第三行?

  • 因为如果传来的对象o是父类的对象,那么父类super还要进行一次无用的判断,所以用第二行给它拒绝掉

为什么父类super会进行一次无用的判断?

  • 因为如果o是父类的对象,那么super中的equals方法察觉不到异常,所以进行了一次判断,但是执行完父类的判断后需要执行子类后序的判断代码,o为父类对象,肯定没有子类元素,而且强转也会出错,所以直接进行super.equals()是不行的,需要先o instanceof Students判断

ok,所以equals方法重写完了吗?并没有,据说instanceof违反了equals的对称性

??????问号脸??????

啥啥啥,这都是啥,equals方法有哪些特性?谁为它定义了这些特性,为什么定义这些特性,是否必须遵守以及不遵守的后果都是啥?

我不寄丢啊,得研究研究《Effective Java》

好了,回到instanceof,上面提到它没有满足对称性

是因为用了instanceof来做比较的话,Stidents.equals(Father)永远不会为真,而Father.equals(Stidents)却有可能为真,这就不对称了,所以干脆就让Father.equals(Stidents)也永远不为真

那要怎么做呢?答案就是instanceof的弟弟:getClass

instanceof用来判断是否为当前类或者子类

getClass只用来判断是否为当前类

改了之后,代码如下:

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

提出问题:getClass()并没有对象调用,它是怎么使用的?

其实这内容就和idea写equals然后弹出的补全代码一样,我们通过还原代码实现的过程,就可以完全理解equals的逻辑:

  1. 判断两个引用指向的对象是否相等
  2. 判断传来的参数是否为空
  3. 判断传来的参数是否属于当前类
  4. 如果有继承父类,则也需要调用父类的super.equals()方法(Object除外)
  5. 最后比较各个属性值是否相等(如果属性为对象引用,则需要通过Objects.equals(a,b)方法来比较引用对象的属性值)

什么是hashCode()方法

hashCode也叫散列码(哈希码),它用来计算对象中所有属性的散列值

关于散列这里就不展开了,我们在这里只需要知道两点:

  1. 散列值为整数,可以为负值
  2. 散列值可以用来确定元素在散列表中的位置(有可能两个元素拥有相同的散列值,这个就是散列冲突)

在Object中,hashCode()是一个本地方法,因为Object没有属性,所以默认返回的是对象的内存地址

代码如下所示:

public class Main {
    public static void main(String[] args) {
        Object t = new Object();
        int a = t.hashCode();
        System.out.println(Integer.toHexString(a)); // 输出 4554617c
    }
}

其中4554617c就是对象a的内存地址,这里转成16进制显示(是因为通常地址都是用16进制显示的,比如我们电脑的Mac地址)

Object的hashCode()方法:

public class Object {
	public native int hashCode();
}

该方法返回该对象的十六进制的哈希码值(即,对象在内存中的数字型名字)。
哈希算法根据对象的地址或者字符串或者数字计算出来的int类型的数值。而且哈希码并不唯一,可保证相同对象返回相同的哈希码,只能尽量保证不同对象返回不同的哈希码值。

native主要用于方法上

1、一个native方法就是一个Java调用非Java代码的接口。一个native方法是指该方法的实现由非Java语言实现,比如用C或C++实现。

2、在定义一个native方法时,并不提供实现体(比较像定义一个Java Interface),因为其实现体是由非Java语言在外面实现的

主要是因为JAVA无法对操作系统底层进行操作,但是可以通过jni(java native interface)调用其他语言来实现底层的访问。

hashCode的几个特性:
  1. 无论hashCode调用多少次,都应该返回一样的结果(这一点跟equals很像)
  2. HashCode的存在主要是用于查找的快捷性,如Hashtable,HashMap等,HashCode经常用于确定对象的存储地址;
  3. 如果两个对象相同, equals方法一定返回true,并且这两个对象的HashCode一定相同;
  4. 两个对象的HashCode相同,并不一定表示两个对象就相同,即equals()不一定为true,只能够说明这两个对象在一个散列存储结构中。
  5. 如果对象的equals方法被重写,那么对象的HashCode也尽量重写,以保证equals方法相等时两个对象hashcode返回相同的值(eg:Set集合中确保自定义类的成功去重)。

Set集合中元素不重复的基本逻辑判断示意图:
重写hashCode和equals方法

equals()方法和hashCode方法的关联

他俩的关系应当是这样的:

  1. 如果两个对象equals,Java运行时环境会认为他们的hashcode一定相等。

  2. 如果两个对象不equals,他们的hashcode有可能相等。

  3. 如果两个对象hashcode相等,他们不一定equals。

  4. 如果两个对象hashcode不相等,他们一定不equals。

hashCode和equals可以说相辅相成的,他俩共同协作用来判断两个对象是否相等

如果分开来看的话,他俩是没什么联系的,但是由于某些原因导致被联系上了(比如HashMap这个小月老)

我们知道 HashMap集合中的key是不能重复的,它是通过equals和hashCode来判断重复的

下面是部分源码:

if (e.hash == hash &&
    ((k = e.key) == key || (key != null && key.equals(k))))
    return e;

可以看到,map先进行hash判断,然后进行equals判断

也就是说,hash是前提,如果hash都不相等,那equals就不用比较了(先计算hash的一个原因是计算hash比equals快得多)

所以我们在自定义对象时,如果覆写了equals,那么一定要记得覆写hashCode:

class Students{
    private int age;
    private String name;
    public Students (int a){
        this.age = a;
    }
    public Students (int a,String b) {this.age = a; this.name = b;}

    //以下是idea软件自动生成的equals方法和hashCode方法
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Students students = (Students) o;
        return age == students.age && Objects.equals(name, students.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(age, name);
    }
}

其中Objects.hash有点类似于上面的Objects.equals()方法,很实用

疑问:如果只覆写了equals,没有覆写hashCode,会咋样呢?

class Students{
private int age;
private String name;
public Students (int a){
  this.age = a;
}
public Students (int a,String b) {this.age = a; this.name = b;}

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

public static void main(String[] args) {
  Students students1 = new Students(20);
  Students students2 = new Students(20);
  Map<Students,Integer> map = new HashMap<>();
  map.put(students1,25);
  map.put(students2,30);
  System.out.println(map.get(students1));
  System.out.println(map.get(students2));
}
}
//输出	25	30
class Students{
 private int age;
 private String name;
 public Students (int a){
     this.age = a;
 }
 public Students (int a,String b) {this.age = a; this.name = b;}

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

 @Override
 public int hashCode() {
     return Objects.hash(age, name);
 }

 public static void main(String[] args) {
     Students students1 = new Students(20);
     Students students2 = new Students(20);
     Map<Students,Integer> map = new HashMap<>();
     map.put(students1,25);
     map.put(students2,30);
     System.out.println(map.get(students1));
     System.out.println(map.get(students2));
 }
}
//输出	30	30

思考:为什么会有不同的输出

​ 原本只有地址相同hashCode值才相同,重写hashCode方法后,放宽了hashCode相同的限制,使之成为equals比较的前置条件

为什么重写equals()方法就要重写HashCode()方法

在Java中的一些容器中,不允许有两个完全相同的对象,插入的时候,如果判断相同则会进行覆盖。这时候如果只重写了equals()的方法,而不重写hashcode的方法,Object中hashcode是根据对象的存储地址转换而形成的一个哈希值。这时候就有可能因为没有重写hashcode方法,造成相同的对象散列到不同的位置而造成对象的不能覆盖的问题。

参考:知乎——Java中的equals()和hashCode() - 超详细篇

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值