equals方法详解

2022-09-21 14:25:17

目录

1、equals方法

 2、重写equals方法的原则是什么

3、相等测试与继承

4、重写了euquals方法为什么还要重写hashcode?

5、如何写好一个equals方法(以下内容摘抄于Java 核心技术 卷1)


1、equals方法

        Object 类中的 equals 方法用于检测一个对象是否等于另一个对象。Object 类中实现的 equals 方法将确定两个对象引用是否相等。看以下代码:

public static void main(String[] args) {
    Object o = new Object();   
    Object o1 = o;
    Object o2 = o;
    System.out.println(o3.equals(o2));
}

代码输出:true

        这是一个合理的默认行为:如果两个对象引用相等,这两个对象就肯定相等。对于很多类来说,这已经足够了,但是还有一种情况:

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;
    }
}

public class Test {
    public static void main(String[] args) {

        Person p1 = new Person("张三");
        Person p2 = new Person("张三");

        System.out.println(p1 == p2);
        System.out.println(p1.equals(p2));
    }
}

代码输出:false false

        有时候基于业务逻辑,我们会需要比较对象内容,但此时的 equals 方法已经满足不了我们的业务要求了,所以我们才会要重写 equals 方法。

        分析: 为什么使用equals方法返回的不是true,而是和 == 相同的结果false呢。这是因为Student这个类并没有重写equals方法,当调用equals方法时,实际上调用的是Object类中的equals方法(Object 类是所有类的父类)。看看equals方法的源码:

public boolean equals(Object obj) {
    return (this == obj);
}

        所以上面的代码运行结果都是false。
        我们要想实现两个对象的状态(内容)相同,就要重写equals方法

重写Person类中的equals方法:

public boolean equals(Object o){
    
    //判断是不是同一对象,是则返回true。
    if(this == o) return true;

    //判断传进来的对象是否为null值,为null则返回false。
    if(o == null) return false;

    //判断两者类型是否相等,如果不等,返回false。
    if(getClass() != o.getClass()) return false;

    //到这一步,o 类型一致,也不为空,进行类型转换,然后才能比较。
    Person p = (Person)o;

    //此处才开始真正进行比较,return后可以&&加条件,就该类的哪些属性相等,才返回true。
    return name.equals(p.name);
    
}

gatClass 方法将返回一个对象所属的类,例

A a = new A();

System.out.println(a.gatClass());  //输出A(会带包名)

此时再次运行,代码输出:false true

在子类中定义 equals 方法时,首先调用父类的 equals。如果检测失败,对象就不可能相等。如果父类中的字段都相等,就需要比较子类中的实例字段。

public class Student extends Person{
    
    private String stuId;  //学号
    ...
    public boolean equals(Object o){

        if(!super.equals(o)) return false;
        //super.equals 检查 this 和 o 是否属于同一类
        
        Student student = (Student)o;
        return stuId.equals(student.stuId);
        //这个equals是用于子类同类比较,如果是子类调用子类equals与父类类型相比,会报
        //ClassCastException异常
    }
}

 2、重写equals方法的原则是什么

Java 语言规范要求 equals 方法具有小面的特性:

        1.自反性:对于任何非空参考值x,x.equals(x)应该返回true。

        2.对称性:对于任何非空参考值x和y,当且仅当y.equals(x)返回true时,x.equals(y)才应返回true。

        3.传递性:对于x,y和z的任何

非空引用值,如果x.equals(y)返回true,而y.equals(z)返回true,则x.equals(z)应该返回true。

        4.一致性:对于任何非空引用值x和y,只要未修改对象的equals比较中使用的信息,对x.equals(y)的多次调用将始终返回true或始终返回false。

        5.对于任何非null参考值x,x.equals(null)应该返回false。

        这些规则当然很合理。你肯定不希望类库实现者在查找数据结构中的一个元素时还要纠结调用 x.equals(y) 还是调用 y.equals(x) 的问题。

一般而言,对于一个类的两个对象,这五个原则都可以满足。看下面的代码:

 public static void main(String[] args) {

     Person p = null;
     Person p1 = new Person("张三");
     Person p2 = new Person("张三");
     Person p3 = new Person("张三");

     System.out.println("自反性:"+p1.equals(p1));  //true
     System.out.println("对称性");
     System.out.println(p1.equals(p2));  //true
     System.out.println(p2.equals(p1));  //true
     System.out.println("传递性");
     System.out.println(p1.equals(p2));  //true
     System.out.println(p2.equals(p3));  //true
     System.out.println(p1.equals(p3));  //true
     System.out.println("一致性:");
     //反复检测
     for(int i =0; i<5;i++){
         if(p1.equals(p2)!=p1.equals(p2)){
             System.out.println("没有遵守一致性");
             break;
         }
     }
     System.out.println("遵守了一致性");
     System.out.println("非空性");
     // 这里的p对象为null,所以返回false
     System.out.println(p1.equals(p));  //false
}

输出结果:

自反性:true
对称性
false
false
传递性
false
false
false
一致性:
遵守了一致性
非空性
false

3、相等测试与继承

如果隐式参数和显示的参数不属于同一个类,equals方法将如何处理?这是一个很有争议的问题。在前面的例子中,如果发现类不匹配,equals 方法返回 false。但是,许多程序员都喜欢使用 instanceof 进行检测,包括我也是。

        if (!(o instanceof Person)) return false;

        这样就允许了 o 属于 Person 子类时,通过。正是因为这样会导致以些麻烦,所以建议不要采用这种处理方式。看下面代码:

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 o) {
        if (this == o) return true;
        if (!(o instanceof Person)) return false;
        Person person = (Person) o;
        return Objects.equals(name, person.name);
    }

}

class Student extends Person{ // 子类

    //子类独有属性
    private String stuId;  //学号

    public Student(String stuId, String name) {
        super(name);
        this.stuId = stuId;
    }

    public String getStuId() {
        return stuId;
    }

    public void setStuId(String stuId) {
        this.stuId = stuId;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Student)) return false;
        if (!super.equals(o)) return false;
        Student student = (Student) o;
        return Objects.equals(stuId, student.stuId);
    }
}

public class Test{

    public static void main(String[] args) {

        Person person = new Person("张三");
        Student student = new Student("0x111", "张三");

        boolean equals1 = person.equals(student);
        boolean equals2 = student.equals(person);

        System.out.println(equals1);
        System.out.println(equals2);
    }
}

代码输出:true
                  false

        为什么出现了false呢,这是因为违背了对称性的原则了。分析一下也不难理解,因为student都是person,所以,per1.equals(stu2)是true,但是反过来就不成立,你不能说所有person都是student,所以stu2.equals(per1)是不成立的。这里也要明确一下父类和子类的equals方法的调用关系。在子类中定义equals方法时,首先调用父类的equals方法,如果检测失败,对象就不可能相等,如果超类中的域都相等,才需要比较子类的实例域。那么上述这个问题怎么解决呢,明白了equals方法的调用关系,那就只需在子类中的eqauls方法中添加一个判断。

// 修改student类中的equals方法
@Override
    public boolean equals(Object o) {
        if (this == o) return true;
        //注释下面判断
        //if (!(o instanceof Student)) return false;
        //两种情况
        //1、o的类型不相关,在父类equals方法执行(!(o instanceof Person))返回false并取反,return false结束
        //2、o的类型相关
        //2.1、假设o为父类:先equals判断父类属性,返回true取反,执行下一步,反之return false结束
        //2.2、假设o为子类:先equals判断父类属性,返回true取反,执行下一步,反之return false结束
        if (!super.equals(o)) return false;
        //3、o为父类,执行下一步
        //3、o为子类进入if语句,进行比较子类私有的属性
        if (o instanceof Student){
            Student student = (Student) o;
            return Objects.equals(getName(), student.getName());
        }
        //4、子类与父类比较,只比较公共属性,相同返回true,反正false
        return super.equals(o);

再运行以上代码:true
                             true

就符合对称性了。

        还有一种情况,分为以下两种情形:

  • 如果子类可以有自己的相等性概念,则对称性需求将强制使用getClass(避免了两个不同类的 equals 比较,会执行o.getClass() != getClass()的if条件判断,如果是两种不同的类型,直接返回false,也避免了父子类 equals 方法发问题)
  • 如果由父类决定相等性概念,那么就可以使用 instanceof 检测,这样可以再不同子类的对象之间进行相等性比较

        例:在学生和人的例子中,只要对应的字段相等,就认为两个对象相等,如果两个 Student 对象的学号相等,而姓名不等,就认为它们是不相同的,因此,我们要使用 getClass检测。

        但是,假设使用人的身份证作为相等性检测标准,并且这个相等性概念适用于所有的子类,就可以使用 instanceof 检测,而且应该将 Person.equals 声明为 final。

4、重写了euquals方法为什么还要重写hashcode?

        看到集合HashMap的底层源码的时候,自然就懂了

看看hashcode的定义:返回对象的哈希码值。支持此方法的好处是可以使用HashMap提供的散列表。

        官方文档明确的说明了此方法的好处是可以使用HashMap提供的散列表。这是因为Map接口的类会使用到键对象的哈希码,当我们调用put方法时,就是根据对象的哈希码来计算存储位置的,因此必须提供对哈希码正确的保证。在java中,可以使用hashCode()这个方法来获取对象的哈希值。

public static void main(String[] args) {
    String s1 = "hello";
    String s2 = "world";
    System.out.println(s1.hashCode());
    System.out.println(s2.hashCode());
}

代码输出:99162322

                  113318802

可以看到不同的对象的hashcode值是不同的。

看看javaAPI的对hashcode的几点说明:

  • 在Java应用程序的执行过程中,无论何时在同一个对象上多次调用它,hashCode方法都必须一致地返回相同的整数,前提是不对对象上的equals比较中使用的信息进行修改。此整数不需要在应用程序的一次执行与同一应用程序的另一次执行之间保持一致。

  • 如果根据equals(Object)方法,两个对象是相等的,那么在每个对象上调用hashCode方法必须产生相同的整数结果。

  • 如果两个对象根据equals(java.lang.Object)方法是不相等的,那么在每个对象上调用hashCode方法必须产生不同的整数结果,这是不需要的。但是,程序员应该意识到,为不相等的对象生成不同的整数结果可能会提高哈希表的性能。

    回到我们前面的问题,为什么重写equals方法,必须要重写hashcode方法。在Object类中,hashCode方法是通过Object对象的地址计算出来的,因为Object对象只与自身相等,所以同一个对象的地址总是相等的,计算取得的哈希码也必然相等。对于不同的对象,由于地址不同,所获取的哈希码自然也不会相等。所以如果一个类重写了equals方法,但没有重写hashCode方法,将会直接违法了第2条规定。还有一点就是重写hashcode()方法,可以方便用户将对象插入到散列表中。

5、如何写好一个equals方法(以下内容摘抄于Java 核心技术 卷1)

        1. 显式参数命名为otherObject,稍后需要将它转换成另一个叫做other的变量。

        2. 检测this与otherObject是否引用同一个对象:if(this=otherObject)returntrue;这条语句只是一个优化。实际上,这是一种经常采用的形式。因为计算这个等式要比一个一个地比较类中的域所付出的代价小得多。

        3. 检测otherObject是否为null,如果为null,返回false。这项检测是很必要的。if(otherObject=null)returnfalse;

        4. 比较this与otherObject是否属于同一个类。如果equals的语义在每个子类中有所改变,就使用getClass检测:if(getClass()!=otherObject.getCIassO)returnfalse;如果所有的子类都拥有统一的语义,就使用instanceof检测:if(!(otherObjectinstanceofClassName))returnfalse;

        5. 将otherObject转换为相应的类类型变量:ClassNameother=(ClassName)otherObject

        6. 现在开始对所有需要比较的域进行比较了。使用=比较基本类型域,使用equals比较对象域。如果所有的域都匹配,就返回true;否则返回false。

        return  fieldl  ==  other.field

                && Objects.equals ( field2 , other.field2)

                && . . .;

        7. 如果在子类中重新定义equals,就要在其中包含调用super.equals(other)。

如果你用的是eclipse或者是idea集成环境的话,可以使用快捷键自动生成符合以上标准的equals()方法,但是有时候需要自定义,所以知道原理很重要。


  • 作者:_小喆
  • 原文链接:https://blog.csdn.net/qq_57887050/article/details/123584838
    更新时间:2022-09-21 14:25:17