equals() 和 == 的区别 以及 重写equals()时是否必须重写hashCode()

一、初步了解在JVM中的内存分配机制

    在JVM中,内存可分为堆内存栈内存,它们两者的区别是:当我们创建一个对象 (new Object) 时,会调用对象的构造方法来开辟空间,将对象数据存储到堆内存中,同时在栈内存中生成对应的引用,当我们在后续代码中调用的时候用的都是栈内存中的引用。而对于方法中声明的基本类型变量 (局部变量),每当程序调用方法时,都会将该变量存储到方法栈内存中,如果是在类中声明的基本类型变量 (全局变量),每当创建对象时,都会将该变量存储到堆内存中。

 

二、equals() 与 == 的区别

        在面试中,我们经常会被问到 equals()方法 与 "=="运算符有什么区别?而我们常常会回答道:"如果使用基本数据类型进行 '=='运算符比较,则比较的是值。如果使用引用类型(对象) 进行 '=='运算符比较,则比较的是地址。而equals()方法比较的是对象的值。",看起来好像是这么回事,但其实我们回答的并不完全正确,接下来就让我们带着这个疑问继续往下看。

    首先说起equals()方法,我们都知道它是超类Object中的一个基本方法,用来判断一个对象与另外一个对象是否相等,而在超类Object的equals()方法中,实际上也是通过 '=='运算符来判断两个对象是否具有相同的引用,其源码如下所示:

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

    实际上我们知道,所有的对象都拥有标识(内存地址) 和 状态(数据),而对象使用"=="运算符比较的是两个对象的内存地址,所以说超类Object 的 equals()方法比较的是两个对象的内存地址是否相等,如果object.equals(otherObject) 结果为true,则表示 object 和 otherObject 实际上是引用的是同一个对象。

     而为什么我们在上述的回答中,会认为equals()方法比较的是对象的值呢?那是因为在日常的项目开发中,我们大多数都是使用equals()方法来对 字符串(String类型) 进行相等判断,而对于String、Integer、Date等类都对超类Object中的 equals()方法进行了重写,比较的是其对象的值,而不再是对象的内存地址,其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;
}

 

    所以对于上述的问题,最终的回答是:"如果使用基本数据类型进行 '=='运算符比较,则比较的是值。如果使用引用类型(对象) 进行 '=='运算符比较,则比较的是地址。而 equals()方法如果比较对象的类没有对其进行重写,则比较的是地址,否则比较的是对象的值。"

    接下来我们来编写一个例子,证明我们上述的结论,代码如下所示:

package com.reflex.test;


public class People {

    private String name;

    public People(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    
    public static void main(String[] args) {
        People people1 = new People("张三");
        People people2 = new People("张三");

        System.out.println("people1.equals(people2)的结果为:" + people1.equals(people2));
        System.out.println("people1 == people2的结果为:" + (people1 == people2));

    }
}

  输出结果为:

people1.equals(people2)的结果为:false
people1 == people2的结果为:false

       对于上述的代码,我们来分析一下:首先对于使用 "=="运算符比较两个People对象,返回的结果为false,这点很容易理解,因为对象使用"=="运算符 比较的是内存地址,而people1 和 people2 使用过new People类的构造方法创建的两个对象,所以people1 和 people2的内存地址自然也就不一样。

      而对于equals()方法比较的两个People对象,我们希望两个人名字相同的情况下即认为他们是同一个人,但是从运行结果来看,people1.equals(people2)返回的结果确是false,对于这个结果我们也能理解,这是因为我们写的People类并没有重写超类Object的 equals()方法,所以People调用的equals() 方法是从超类Object 继承过来的,根据我们之的分析 超类Object的 equals()方法内部实现使用的是 "=="运算符,比较的是两个对象的内存地址是否相同,所以运行结果返回了false。

     因此为了达到我们的期望 (即两个人名字相同的情况下即认为他们是同一个人),我们必须对超类Object的 equals()方法进行重写,让其比较的是对象的名称 (即对象的内容),而不是比较内存地址,于是修改如下:

package com.reflex.test;


public class People {

    private String name;

    public People(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 People){
            People people = (People)obj;
            return name.equals(people.getName());
        }

        return false;
    }

    public static void main(String[] args) {
        People people1 = new People("张三");
        People people2 = new People("张三");

        System.out.println("people1.equals(people2)的结果为:" + people1.equals(people2));
    }
}

  输出结果为:

people1.equals(people2)的结果为:true

    对超类Object的 equals()方法进行重写,使用 instanceof来判断引用obj所指向的对象的类型,如果obj是People类的对象,则将其强制转为People对象,然后比较两者People的名字,相等返回true,否则返回false。当然如果obj不是 People对象,自然也得返回false。

 

三、重写equals()时是否必须重写hashCode()

    最初遇到这个问题时,我也很好奇,我们已经可以通过重写超类Object的 equals()方法来判断两个对象的内容是否相等,还有必要重写hashCode()方法吗,而hashCode()方法是干嘛的又能帮助我们进行什么样的操作呢?带着这个问题我们继续往下看,hashCode()方法主要是针对映射相关的操作 (HashTable、HashMap和HashSet) 使用的,学过数据结构的朋友都知道,类似HashMap这样的结构都会使用到键对象的哈希码,当我们调用put()方法或者get()方法时,都会根据键对象的哈希码来计算值的存储位置。在java中 hashCode()方法同样也是超类Object中的一个基本方法,是返回对象存储在内存地址的编号,由于是超类Object中的基本方法,因此所有的子类都有该方法。接下来就让我们认识一下hashCode()方法吧。

package com.reflex.test;


public class HashCodeTest {

    public static void main(String[] args) {

        String str = "num";
        StringBuilder sBuilder = new StringBuilder(str);
        System.out.println("str的哈希码为:" + str.hashCode() + ",sBuilder的哈希码为:" + sBuilder.hashCode());

        String myStr = new String("num");
        StringBuilder stBuilder = new StringBuilder(myStr);
        System.out.println("myStr的哈希码为:" + myStr.hashCode() + ",stBuilder的哈希码为:" + stBuilder.hashCode());
        
    }

}

 输出结果为:

str的哈希码为:109446,sBuilder的哈希码为:356573597
myStr的哈希码为:109446,stBuilder的哈希码为:1735600054

    可以看到,字符串str 与 myStr拥有相同的哈希码,这是因为String类对超类Object的 hashCode()方法进行了重写,其字符串的哈希码是由内容导出的 (即内容相同,则哈希码相同),而sBuilder 和 stBuilder却拥有不同的哈希码,这是因为StringBuilder没有重写hashCode()方法,使用的是超类Object的 hashCode()方法,而该方法时通过对象的存储地址计算出来的,自然哈希码也就不同。

 

如果我们不重写超类Object的 hashCode()方法,在进行集合操作时又会发生怎么样的情况呢?请看下面这个例子

package com.reflex.test;

import java.util.HashMap;


public class HashCodeTest {

    private String name;

    public HashCodeTest(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "HashCodeTest{" +
                "name='" + name + '\'' +
                '}';
    }

    public static void main(String[] args) {

        HashMap<String, HashCodeTest> stringHashMap = new HashMap<String, HashCodeTest>(2);
        String s1 = new String("key");
        String s2 = new String("key");
        HashCodeTest hashCodeTest = new HashCodeTest("my HashCode");

        stringHashMap.put(s1, hashCodeTest);

        System.out.println("s1.equals(s2)的结果为:" + s1.equals(s2));
        System.out.println("hashMap.get(s1)的结果为:" + stringHashMap.get(s1));
        System.out.println("hashMap.get(s2)的结果为:" + stringHashMap.get(s2));


        //-------------------------------------------------------------------------
        HashMap<Key, HashCodeTest> keyHashMap = new HashMap<Key, HashCodeTest>(2);
        Key k1 = new Key("key");
        Key k2 = new Key("key");

        keyHashMap.put(k1, hashCodeTest);

        System.out.println("k1.equals(k2)的结果为:" + k1.equals(k2));
        System.out.println("hashMap.get(k1)的结果为:" + keyHashMap.get(k1));
        System.out.println("hashMap.get(k2)的结果为:" + keyHashMap.get(k2));

    }

}


class Key {

    private String value;

    public Key(String value) {
        this.value = value;
    }

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }

    @Override
    public boolean equals(Object obj) {

        if (obj instanceof Key) {
            Key key = (Key) obj;
            return value.equals(key.getValue());
        }

        return false;
    }
}

输出结果为:

s1.equals(s2)的结果为:true
hashMap.get(s1)的结果为:HashCodeTest{name='my HashCode'}
hashMap.get(s2)的结果为:HashCodeTest{name='my HashCode'}
k1.equals(k2)的结果为:true
hashMap.get(k1)的结果为:HashCodeTest{name='my HashCode'}
hashMap.get(k2)的结果为:null

        对于s1和s2的结果,很好理解,因为通过相同的键s1 和 s2,在HashMap集合中获取相同内容为HashCodeTest{name='my HashCode'}这很正常,因为String类重写了equals()方法 和 hashCode()方法,使其比较的是内容 和 获取内容的哈希码,但是对于k1 和 k2的结果就让人很意外了,为什么k1.equals(k2)返回的结果为 true,但是k1可以获取到内容,而k2获取的内容却为null,这是为什么呢?想必大家已经发现了,Key类只重写了equals()方法,但是并没有重写hashCode()方法,所以equals()方法比较的是内容,而hashCode()方法呢?由于Key类没有重写,那肯定调用的是超类Object的 hashCode()方法,返回对象存储在内存地址的编号,又因为k1 和 k2是两个不同的对象,所以地址肯定不一样,所以导致我们在通过 keyHashMap.get(k2) 获取值时为null。那该如何修改呢?其实也很简单,在Key类中重写超类Object的 hashCode()方法即可,代码如下:

package com.reflex.test;

import java.util.HashMap;


public class HashCodeTest {

    private String name;

    public HashCodeTest(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "HashCodeTest{" +
                "name='" + name + '\'' +
                '}';
    }

    public static void main(String[] args) {

        HashMap<Key, HashCodeTest> keyHashMap = new HashMap<Key, HashCodeTest>(2);

        Key k1 = new Key("key");
        Key k2 = new Key("key");
        HashCodeTest hashCodeTest = new HashCodeTest("my HashCode");

        keyHashMap.put(k1, hashCodeTest);

        System.out.println("k1.equals(k2)的结果为:" + k1.equals(k2));
        System.out.println("hashMap.get(k1)的结果为:" + keyHashMap.get(k1));
        System.out.println("hashMap.get(k2)的结果为:" + keyHashMap.get(k2));

    }

}


class Key {

    private String value;

    public Key(String value) {
        this.value = value;
    }

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }

    @Override
    public boolean equals(Object obj) {

        if (obj instanceof Key) {
            Key key = (Key) obj;
            return value.equals(key.getValue());
        }

        return false;
    }

    @Override
    public int hashCode() {
        return value.hashCode();
    }
}

输出结果为:

k1.equals(k2)的结果为:true
hashMap.get(k1)的结果为:HashCodeTest{name='my HashCode'}
hashMap.get(k2)的结果为:HashCodeTest{name='my HashCode'}

 

   好了,到此hashCode()就介绍完了,回归到我们之前的问题,重写equals()时必须重写hashCode(),同时在Java API文档中关于hashCode()方法有以下介绍和几点规定:

    介绍:public int hashCode()返回该对象的哈希码值。支持此方法是为了提高哈希表(例如 Java.util.Hashtable 提供的哈希表)的性能。

   规定:

      1、在 Java 应用程序执行期间,在对同一对象多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是将对象进行 equals 比较时所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。

      2、如果根据 equals(Object) 方法,判断两个对象是相等的,那么对这两个对象中的每个对象调用 hashCode() 方法都必须生成相同的整数结果。

     3、如果根据 equals(java.lang.Object) 方法,两个对象不相等,那么对这两个对象中的任一对象上调用 hashCode 方法不要求一定生成不同的整数结果。但是,程序员应该意识到,为不相等的对象生成不同整数结果可以提高哈希表的性能。

   总结起来就是说:equals()相等的两个对象,它们的hashCode()肯定相等,但是hashCode()相等的两个对象,它们的equals()不一定相等。

 

    在使用ORM( 对象关系映射(Object Relational Mapping,简称ORM)是通过使用描述对象和数据库之间映射的元数据,将面向对象语言程序中的对象自动持久化到关系数据库中 )时,需要注意的是:

   1、如果你使用ORM处理一些对象的话,你要确保在类的 hashCode()和equals()中使用getter和setter而不是直接引用成员变量。因为在ORM中有的时候成员变量会被延时加载,这些变量只有当getter方法被调用的时候才真正可用。

   2、例如:如果我们在类的hashCode()和equals()中使用的是 name == people.name则可能会出现这个问题,但是我们使用this.getName() == people.getName()就不会出现这个问题。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值