让你彻底搞懂,为什么重写equals()方法的时候要重写hashCode()方法
发表于:2019-07-30 | 分类: 面试

为什么重写equals()方法的时候要重写hashCode()方法,这次总算弄明白了

一、equals()方法

先说说equals()方法。
Java中Object类中的equals()方法,源码如下:

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

可以看到这里直接用’==’来直接比较

我们知道Java有8种基本类型:数值型(byte、short、int、long、float、double)、字符型(char)、布尔型(boolean),对于这8种基本类型的比较,变量存储的就是值,所以比较的就是’值’本身。如下,值相等就是true,不等就是false。

public class ValueCompare {
    public static void main(String[] args) {
        int a=3;
        int b=4;
        int c=3;
        System.out.println(a==b);   //false
        System.out.println(a==c);   //true
    }
}

对于非基本类型,也就是常说的引用数据类型:类、接口、数组,由于变量中存储的是内存地址,并不是’值’本身,所以真正比较的是该变量存储的地址,可想而知,如果声明的时候是2个对象,地址固然不同, 使用==号比较就会是false。

public class ReferenceCompare {
    public static void main(String[] args) {
        String str1 = new String("123");
        String str2 = new String("123");
        System.out.println(str1 == str2);  //false
    }
}

可以看到,上面这种比较方法,和Object类中的equals()方法的具体实现相同,之所以为false,是因为直接比较的是str1和str2指向的地址,也就是说Object中的equals方法是直接比较的地址,因为Object类是所有类的基类,所以调用新创建的类的equals方法,比较的就是两个对象的地址。那么就有人要问了,如果就是想要比较引用类型实际的值是否相等,该如何比较呢?

要解决上面的问题,就是今天要说的equals(),具体的比较由各自去重写,比较具体的值的大小。我们可以看看上面字符串的比较,如果调用String的equals方法的结果。

public class ReferenceCompare {
    public static void main(String[] args) {
        String str1 = new String("123");
        String str2 = new String("123");
        System.out.println(str1.equals(str2));  //true
    }
}

可以看到比较的结果返回的是true,我们可以看一下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;
}

可以看到String底层是对其从父类Object类中继承的equals方法进行了重写

重写后的比较逻辑是,

  1. 先使用==号比较两个对象的内存地址是否一致, 如果一致则直接返回true.
  2. 如果不一致, 接着再使用instanceof方法判断调用equals方法的对象是否是String类的一个实例, 如果不是则直接返回false
  3. 如果是String类的实例, 则将该对象先强转成String对象, 接着先比较两个字符串的长度是否相等,不相等直接返回false,
  4. 相等再继续比较, 将字符串拆成一个一个的字符依次比较, 如果每个对应位置上的字符都相等才返回true, 否则返回false

由此可知, String类通过对Object类的equals方法进行了重写, 使用重写后的equals方法进行比较时, 不管两个字符串的内存地址一不一样, 只要值相等就一定相等

二、只重写equals方法会造成的问题

接着, 我们来看一下只重写equals方法时, 会造成什么问题

首先, 自定义一个Student类, 重写equals方法, 使得该类使用equals方法比较不管内存地址相不相等, 只要姓名,年龄,身份证号都相等就一定相等, 是同一个人

public class Student {

    private String name;   //姓名
    private int age;       //年龄
    private String idCard; //身份证号

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

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

接下来, 看看我们的测试类

public class Test {
    public static void main(String[] args) {
        Student student1 = new Student("马云",20,"123");
        Student student2 = new Student("马云",20,"123");
        System.out.println(student1.equals(student2));   //true
        System.out.println(student1.hashCode());         //1163157884
        System.out.println(student2.hashCode());        //1956725890
        HashMap<Student, String> map = new HashMap<>();
        map.put(student1, "abc");
        map.put(student2, "def");
        System.out.println(map.get(student1)); //abc
        System.out.println(map.get(student2)); //def
    }
}

我们知道hashmap中键是唯一的, 不允许两个相同的键出现.

但从上述代码的打印结果我们可以看出, 在我们认为的是同一个对象的student1和student2, 在HashMap看来不是一个相同的key

发现出现了矛盾???

用equals比较说明对象相同,但是在HashMap中却以不同的对象存储

(没有重写hascode值,两个hascode值,在他看来就是两个对象)。

到底这两个对象相等不相等????

三、hashmap存取value元素的原理

我们想要搞清楚为什么会造成上述矛盾的地方, 就得知道hashmap存取value元素的原理

(1) 向HashMap存储kv数据的时候,会首先调用k对象的hashCode方法计算k的哈希值, 然后再拿该hash值&上自身无符号右移16位的值定位元素在数组中要存储的位置,

我们分析上面那段代码,HashMap的key是我们自己定义的一个类,可以看到,我们没有重写hashCode方法,意思是hashmap在进行存储的时候是调用的Object类中的hashCode()方法。

而Object类中的hashCode()方法底层是一个本地方法只要对象在堆内存中的地址不同, 则调用这个本地方法返回的哈希值就不同, 由此, 两个student对象就被存储到了hashmap中的不同位置

public native int hashCode();

四、hashCode()方法

修改后的Student类

public class Student {

    private String name;   //姓名
    private int age;        //年龄
    private String idCard; //身份证号

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

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

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

再次执行测试类的main方法查看输出结果

public class Test {
    public static void main(String[] args) {
        Student student1 = new Student("马云",20,"123");
        Student student2 = new Student("马云",20,"123");
        System.out.println(student1.equals(student2));   //true
        System.out.println(student1.hashCode());         //1197105506
        System.out.println(student2.hashCode());         //1197105506
        HashMap<Student, String> map = new HashMap<>();
        map.put(student1, "abc");
        map.put(student2, "def");
        System.out.println(map.get(student1)); //def
        System.out.println(map.get(student2)); //def
    }
}

可以看到, 此时相同的两份对象存储到hashmap中就只存储了一份了

五、查看Object类中的hashcode方法, 查看Objects工具类中的hashcode方法

Objects工具类中的hash方法

public static int hash(Object... values) {
    return Arrays.hashCode(values);
}

Arrays工具类中的hashCode方法

public static int hashCode(Object a[]) {
    if (a == null)
        return 0;

    int result = 1;

    for (Object element : a)
        result = 31 * result + (element == null ? 0 : element.hashCode());

    return result;
}

Object类中的hashcode方法底层调用的是本地native方法

public native int hashCode();

六、总结

为什么重写 equals 时必须重写 hashCode 方法?

我们重写equals时,是为了用自身的方式去判断两个自定义对象是否相等,然而如果此时刚好需要我们用自定义的对象去充当hashmap的键值使用时,就会出现我们认为的同一对象,却因为hash值不同而导致hashmap中存了两个对象,从而才需要进行hashcode方法的重写

(注意: HashMap、HashTable、HashSet底层都是采用的这种方式存储值的)

上一篇:
Nginx
下一篇:
Log4j & Logback