详解重写equals()和hashcode()

本文详细阐述了为何要在Java中重写equals()和hashcode()方法,包括重写的原因、遵循的原则以及具体实现方法。内容涉及自反性、对称性、传递性、一致性等原则,并通过示例解释了两个对象有相同hashcode值但不一定相等的原因。文章还探讨了如何避免重写过程中的常见问题,以确保对象比较的正确性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

为什么要重写equals( )

 判断两个对象在逻辑上是否相等,如根据类的成员变量来判断两个类的实例是否相等,而继承Object中的equals方法只能判断两个引用变量是否是同一个对象,这样我们往往需要重写equals()方法。
 我们在向一个没有重复对象的集合中添加元素时,集合中存放的往往是对象,我们需要先判断集合中是否存在已知对象,这样就必须重写equals( )

怎样重写equals( )

重写equals( )必须遵循的要求如下:

1. 自反性: 对于任何非空引用x,x.equals(x)应该返回true。
2. 对称性: 对于任何引用x和y,如果x.equals(y)返回true,那么y.equals(x)也应该返回true。
3. 传递性: 对于任何引用x、y和z,如果x.equals(y)返回true,y.equals(z)返回true,那么x.equals(z)也应该返回true。
4. 一致性: 如果x和y引用的对象没有发生变化,那么反复调用x.equals(y)应该返回同样的结果。
5. 非空性: 对于任意非空引用x,x.equals(null)应该返回false。

1. 自反性原则

在JavaBean中,经常会覆写equals方法,从而根据实际业务情况来判断两个对象是否相等,比如我们写一个person类,根据姓名来判断两个person类实例对象是否相等。代码如下:

public class Person {
    private String name;
  
    public Person(String name){
        this.name = name;
    }
  
    public String getName() {
        return name;
    }
  
    public void setName(String name) {
        this.name = name;
    }
  
    @Override
    public boolean equals(Object obj) {
        if (obj instanceof Person) {
            Person person= (Person) obj;
            return name.equalsIgnoreCase(person.getName().trim());
        }
        return false;
    }
    public static void main(String[] args){
        Person p1=new Person("张三");
        Person p2=new Person("张三    ");
        List<Person> list = new ArrayList<Person>();
        list.add(p1);
        list.add(p2);
        System.out.println("是否包含张三:"+list.contains(p1)); //true
        System.out.println("是否包含张三:"+list.contains(p2)); //false
    }
}

list中均含有这个两个生成的person对象,结果应该都为true,但是实际结果:

  • 是否包含张三:true
  • 是否包含张三:false

第二个为什么会是false呢?
 原因在于list中检查是否含有元素时是通过调用对象的equals方法来判断的,也就是说 contains(p2)传递进去会依次执行p2.equals(p1)、p2.equals(p2),只要一个返回true,结果就是true。但是这里p2.equals(p2)返回的是false?由于我们对字符前后进行了空格的切割造成p2.equals(p2)的比较实际上是:“张三 ”.equals(“张三”),一个有空格,一个没有空格就出错了。
 这个违背了equals的自反性原则:对于任何非空引用x,x.equals(x)应该返回true。
 这里只要去掉trim方法就可以解决。

2. 对称性和非空性原则

上面这个例子,还并不是很好,如果我们传入null值,会怎么样呢?

Person p2=new Person(null);	//往对象传入空值

是否包含张三:true //输出结果
Exception in thread "main" java.lang.NullPointerException

为什么会返回true且报空指针?
 原因在执行p2.equals(p1)时,由于p2的name是一个null值,所以调用name.equalsIgnoreCase()方法时就会报空指针异常。

 这是在覆写equals方法时没有遵循对称性原则:对于任何应用x,y的情形,如果想x.equals(y)返回true,那么y.equals(x),也应该返回true。

 应该在equals方法里加上是否为null值的判断:

@Override
    public boolean equals(Object obj) {
        if (obj instanceof Person) {
            Person person= (Person) obj;
            if (person.getName() == null || name == null) {
                return false;
            }else{
                return name.equalsIgnoreCase(person.getName());
            }
        }
        return false;
    }

3. 传递性原则

/**
 * 假设现在我们有一个Employee类继承自person类
*/
public class Employee extends Person{
    private int id;
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public Employee(String name,int id) {
        super(name);
        this.id = id;
        // TODO Auto-generated constructor stub
    }
    @Override
    public boolean equals(Object obj) {
        if(obj instanceof Employee){
            Employee e = (Employee)obj;
            return super.equals(obj) && e.getId() == id;
        }
        return super.equals(obj);
    }
    public static void main(String[] args){
        Employee e1=new Employee("张三",12);
        Employee e2=new Employee("张三",123);
        Person p1 = new Person("张三");
  
        System.out.println(p1.equals(e1)); //true
        System.out.println(p1.equals(e2)); //true
        System.out.println(e1.equals(e2)); //false
    }
}

 只有在name和ID都相同的情况下才是同一个员工,避免同名同姓的。在main里定义了,两个员工和一个社会闲杂人员,虽然同名同姓但肯定不是同一个人。运行结果应该三个都是false才对。但是输出结果为:true;true;false;

 p1尽然等于e1,也等于e2,不是同一个类的实例也相等了?因为p1.equals(e1)是调用父类的equals方法进行判断的它使用instanceof关键字检查e1是否是person的实例,由于employee和person是继承关系,结果就是true了。但是放过来就不成立,e1,e2就不等于p1,这也是违反对称性原则的一个典型案例。

 e1竟然不等于e2?e1.equals(e2)调用的是Employee的equals方法,不仅要判断姓名相同还有判断工号相同,两者的工号不同,不相等时对的。但是p1等于e1,也等于e2,e1却不等于e2,这里就存在矛盾,等式不传递是因为违反了equals的传递性原则:对于实例对象x、y、z;如果x.equals(y)返回true,y.equals(z)返回true,那么x.equals(z)也应该返回true。

 上述情况会发生是因为父类使用instanceof关键字(是否是这个特定类或者是它的子类的一个实例),用来判断是否是一个类的实例对象的,这很容易让子类“钻空子”。想要解决也很简单,使用getClass进行类型的判断,person类的equals方法修改如下:

@Override
    public boolean equals(Object obj) {
        if (obj != null && obj.getClass() == this.getClass()) {
            Person person= (Person) obj;
            if (person.getName() == null || name == null) {
                return false;
            }else{
                return name.equalsIgnoreCase(person.getName());
            }
        }
        return false;
    }

4. 重写equals( )必须重写hashcode( )

 理由参考如下板块

为什么要重写hashcode( )

  • 用以维护 hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码。如下:
    (1)当obj1.equals(obj2)为true时,obj1.hashCode() == obj2.hashCode()必须为true

    (2)当obj1.hashCode() == obj2.hashCode()为false时,obj1.equals(obj2)必须为false

  • 当你把对象加入HashSet时,HashSet会先计算对象的hashcode值来判断对象加入的位置,同时也会与其他已经加入的对象的hashcode值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同hashcode值的对象,这时会调用equals()方法来检查hashcode相等的对象是否真的相同。如果两者相同,HashSet就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了equals的次数,相应就大大提高了执行速度。

为什么两个对象有相同的hashcode值,它们也不一定是相等?

因为hashCode() 所使用的杂凑算法也许刚好会让多个对象传回相同的杂凑值。越糟糕的杂凑算法越容易碰撞,但这也与数据值域分布的特性有关(所谓碰撞也就是指的是不同的对象得到相同的 hashCode)。

我们刚刚也提到了 HashSet,如果 HashSet 在对比的时候,同样的 hashcode 有多个对象,它会使用 equals() 来判断是否真的相同。也就是说 hashcode 只是用来缩小查找成本。

怎样重写hashcode( )

1. 重写hashcode( )的原则

  1. 同一个对象多次调用hashCode()方法应该返回相同的值;
  2. 当两个对象通过equals()方法比较返回true时,这两个对象的hashCode()应该返回相等的(int)值;
  3. 对象中用作equals()方法比较标准的Filed(成员变量(类属性)),都应该用来计算hashCode值。

2. 计算hashCode值的方法:

//f是Filed属性
boolean    hashCode=(f?0:1)
(byte,short,char,int)      hashCode=(int)f
long       hashCode=(int)(f^(f>>>32))
float       hashCode=Float.floatToIntBits(f)
double   hashCode=(int)(1^(1>>>32))
普通引用类型    hashCode=f.hashCode()

将计算出的每个Filed的hashCode值相加返回,为了避免直接相加产生的偶然相等(单个不相等,加起来就相等了),为每个Filed乘以一个质数后再相加,例如有:

 return  f1.hashCode()*17+(int)f2.13

3. 常用示例

1.经典示例,这种17和31散列码的想法来自经典的Java书籍——《Effective Java》第九条。

public class User {
    private String name;
    private int age;
    private String passport;
    //getters and setters, constructor
    @Override
    public boolean equals(Object o) {
        if (o == this) return true;
        if (!(o instanceof User)) {
            return false;
        }
        User user = (User) o;
        return user.name.equals(name) &&
                user.age == age &&
                user.passport.equals(passport);
    }
    //Idea from effective Java : Item 9
    @Override
    public int hashCode() {
        int result = 17;
        result = 31 * result + name.hashCode();
        result = 31 * result + age;
        result = 31 * result + passport.hashCode();
        return result;
    }
}

2.对于JDK7及更新版本,你可以是使用java.util.Objects 来重写 equals 和 hashCode 方法,代码如下

import java.util.Objects;
 
public class User {
    private String name;
    private int age;
    private String passport;
 
    //getters and setters, constructor
 
    @Override
    public boolean equals(Object o) {
        if (o == this) return true;
        if (!(o instanceof User)) {
            return false;
        }
        User user = (User) o;
        return age == user.age &&
                Objects.equals(name, user.name) &&
                Objects.equals(passport, user.passport);
    }
 
    @Override
    public int hashCode() {
        return Objects.hash(name, age, passport);
    }
 
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值