关于重写equals()和hashCode()的思考

释放双眼,带上耳机,听听看~!

最近这几天一直对equals()和hashCode()的事搞不清楚,云里雾里的。

为什么重写equals(),我知道。

但是为什么要两个都要重写呢,我就有点迷糊了,所以趁现在思考清楚后记录一下。

起因

无非就是一道面试题:“你重写过 equalshashcode 吗,为什么重写equals时必须重写hashCode方法?”

 

1.为什么要重写equals()

也不用多说大道理了,我们都知道Object类的equals()其实用的也是“==”。

关于重写equals()和hashCode()的思考

我们也知道“==”比较的是内存地址。

所以当对象一样时,它的内存地址也是一样的,所以此时不管是“==”也好,equals()也好,都是返回true。

例子

public static void main(String[] args) {
        String s = \"大木大木大木大木\";
        String sb = s;

        System.out.println(s.equals(sb));

        System.out.println(s == sb);
    }

输出结果

true
true

但是,我们有时候判断两个对象是否相等不一定是要判断它的内存地址是否相等,我只想根据对象的内容判断。

在我们人为的规定下,我看到对象的内容相等,我就认为这两个对象是相等的,怎么做呢?

很显然,用“==”是做不到的,所以我们需要用到equals()方法,

我们需要重写它,让它达到我们上面的目的,也就是根据对象内容判断是否相等,而不是根据对象的内存地址。

例子:没有重写equls()

public class MyClass {
public static void main(String[] args) { Student s1 = new Student(\"jojo\", 18); Student s2 = new Student(\"jojo\", 18); System.out.println(s1.equals(s2)); } private static class Student { String name; int age; public Student(String name, int age) { this.name = name; this.age = age; } } }

输出结果

false

结果分析

两个长得一样的对象比较,为什么equals()会返回false。

因为我们的Student类没有重写equals()方法,所以它调用的其实是Object类的equals(),其实现就是“==”。

所以虽然两个对象长一样,但它们的内存地址不一样,两个对象也就不相等,所以就返回了false。

 

所以我们为了达到我们先前的规定,需要重写一下equals()方法,至于重写此方法,有几点原则,我就不多说了,都是些废话。

例子:重写了equals()方法

 private static class Student {
        String name;
        int age;

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

     /**
     * 重写后的equals()是根据内容来判定相等的
     */
@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 && name.equals(student.name); } }

此时再次执行以下代码

public static void main(String[] args) {
        Student s1 = new Student(\"jojo\", 18);
        Student s2 = new Student(\"jojo\", 18);

        System.out.println(s1.equals(s2));
    }

输出结果

true

结果分析

显然我们此时已经达到了目的,根据内容能够判定对象相等。

 

总结

1.Object类的equals()方法实现就是使用了\”==\”,所以如果没有重写此方法,调用的依然是Object类的equals()

2.重写equals()是为了让对象根据内容判断是否相等,而不是内存地址。

 

2.哈希值

关于hashCode(),百度百科是这么描述的。

hashCode是jdk根据对象的地址或者字符串或者数字算出来的int类型的数值 
详细了解请 参考 public int hashCode()返回该对象的哈希值。
支持此方法是为了提高哈希表(例如 java.util.Hashtable 提供的哈希表)的性能。

哈希表的内容我这里也不多提,但我们需要明白的一点是,

哈希值相当于一个元素在哈希表中的索引位置。

其中,

元素的哈希值相同,内容不一定相同

元素的内容相同,哈希值一定相同。

为什么第一句会这么说呢,因为其就是接下来所说的哈希冲突。

 

3.哈希冲突

哈希冲突就是哈希值重复了,撞车了。

最直观的看法是,两个不同的元素,却因为哈希算法不够强,算出来的哈希值是一样的。

所以解决哈希冲突的方法就是哈希算法尽可能的强。

例子:弱的哈希算法

public class MyClass {
    public static void main(String[] args) {
        //Student对象
        Student s1 = new Student(\"jojo\", 18);
        Student s2 = new Student(\"JOJO\", 18);//用equals()比较,并附带hashCode()
        System.out.println(\"哈希值:s1-->\" + s1.hashCode() + \"  s2-->\" + s2.hashCode());
    }

    private static class Student {
        String name;
        int age;

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

        /**
         * 此方法只是简单的运算了name和age。
         * @return
         */
        @Override
        public int hashCode() {
            int nameHash = name.toUpperCase().hashCode();
            return nameHash ^ age;
        }
    }
}

输出结果

哈希值:s1-->2282840  s2-->2282840

结果分析

我们可以看到这个两个不同的对象却因为简单的哈希算法不够健壮,导致了哈希值的重复。

这显然不是我们所希望的,所以可以用更强的哈希算法。

例子:强的哈希算法

public class MyClass {
    public static void main(String[] args) {
        //Student对象
        Student s1 = new Student(\"jojo\", 18);
        Student s2 = new Student(\"JOJO\", 18);

        //用equals()比较,并附带hashCode()
        System.out.println(\"哈希值:s1-->\" + s1.hashCode() + \"  s2-->\" + s2.hashCode());
    }

    private static class Student {
        String name;
        int age;

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

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

输出结果

哈希值:s1-->101306313  s2-->70768585

结果分析

上面用的哈希算法是IDEA自动生成的,它是使用了java.util.Objects的hash()方法,

总而言之,好的哈希算法能够尽可能的减少哈希冲突。

 

总结

哈希冲突可以减少,但无法避免,解决方法就是哈希算法尽可能的强。

 

所以结合上面而言,我们可以认为,哈希值越唯一越好,这样在哈希表中插入对象时就不容易在同一个位置插入了。

但是,我们希望哈希值唯一,现实却不会如我们希望,在哈希表中,哈希码值的计算总会有撞车,有重复的,

关于哈希值的介绍仅此这么点,更多详情可以继续学习,这里就不多提及了。

 

4.极其重要的一点

不是所有重写equals()的都要重写hashCode(),如果不涉及到哈希表的话,就不用了,比如Student对象插入到List中。

涉及到哈希表,比如HashSet, Hashtable, HashMap这些数据结构,Student对象插入就必须考虑哈希值了。

 

5.重写

以下的讨论是设定在jdk1.8的HashSet,因为HashSet本质是哈希表的数据结构,是Set集合,是不允许有重复元素的。

在这种情况下才需要重写equals()和重写hashCode()。

我们分四种情况来说明。

1.两个方法都不重写

例子

public class MyClass {

    public static void main(String[] args) {
        //Student对象
        Student s1 = new Student(\"jojo\", 18);
        Student s2 = new Student(\"jojo\", 18);

        //HashSet对象
        HashSet set = new HashSet();
        set.add(s1);
        set.add(s2);

        //输出两个对象
        System.out.println(s1);
        System.out.println(s2);

        //输出equals
        System.out.println(\"s1.equals(s2)的结果为:\" + s1.equals(s2));

        //输出哈希值
        System.out.println(\"哈希值为:s1->\" + s1.hashCode() + \"  s2->\" + s2.hashCode());

        //输出set
        System.out.println(set);
    }

    private static class Student {
        String name;
        int age;

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

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

输出结果

Student{name=\’jojo\’, age=18}
Student{name=\’jojo\’, age=18}
s1.equals(s2)的结果为:false
哈希值为:s1->2051450519 s2->99747242
[Student{name=\’jojo\’, age=18}, Student{name=\’jojo\’, age=18}]

结果分析

equals()返回 false

哈希值          不一致

两个都没有重写,HashSet将s1和s2都存进去了,明明算是重复元素,为什么呢?

到底HashSet怎么才算把两个元素视为重复呢?

 

2.只重写equals()

例子

public class MyClass {

    public static void main(String[] args) {
        //Student对象
        Student s1 = new Student(\"jojo\", 18);
        Student s2 = new Student(\"jojo\", 18);

        //HashSet对象
        HashSet set = new HashSet();
        set.add(s1);
        set.add(s2);

        //输出两个对象
        System.out.println(s1);
        System.out.println(s2);

        //输出equals
        System.out.println(\"s1.equals(s2)的结果为:\" + s1.equals(s2));

        //输出哈希值
        System.out.println(\"哈希值为:s1->\" + s1.hashCode() + \"  s2->\" + s2.hashCode());

        //输出set
        System.out.println(set);
    }

    private static class Student {
        String name;
        int age;

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

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

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

输出结果

Student{name=\'jojo\', age=18}
Student{name=\'jojo\', age=18}
s1.equals(s2)的结果为:true
哈希值为:s1->2051450519  s2->99747242
[Student{name=\'jojo\', age=18}, Student{name=\'jojo\', age=18}]

结果分析

equals()返回 true

哈希值          不一致

重写了equals()之后,HashSet依然将s1和s2都存进去了。

哈希值不一样,按照哈希值即索引的说法,所以HashSet存入了两个索引不同的元素?

又或者是HashSet判断元素重复不是靠equals()?我们接着往下看

 

3.只重写hashCode()

例子

public class MyClass {

    public static void main(String[] args) {
        //Student对象
        Student s1 = new Student(\"jojo\", 18);
        Student s2 = new Student(\"jojo\", 18);

        //HashSet对象
        HashSet set = new HashSet();
        set.add(s1);
        set.add(s2);

        //输出两个对象
        System.out.println(s1);
        System.out.println(s2);

        //输出equals
        System.out.println(\"s1.equals(s2)的结果为:\" + s1.equals(s2));

        //输出哈希值
        System.out.println(\"哈希值为:s1->\" + s1.hashCode() + \"  s2->\" + s2.hashCode());

        //输出set
        System.out.println(set);
    }

    private static class Student {
        String name;
        int age;

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

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

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

输出结果

Student{name=\'jojo\', age=18}
Student{name=\'jojo\', age=18}
s1.equals(s2)的结果为:false
哈希值为:s1->101306313  s2->101306313
[Student{name=\'jojo\', age=18}, Student{name=\'jojo\', age=18}]

结果分析

equals()返回 false

哈希值          一致

这次我们只重写hashCode(),没有重写equals(),

我们可以看到s1和s2的哈希值是相同的,怎么还会存储两个对象呢,不应该啊。

 

难道之前哈希值是索引的说法是错的?不是这样的,这个说法没有错。

但也不是绝对正确,在同一个哈希值下,如果两个元素的内容不一样,依然会都被存储起来。

但是先不说在强的哈希算法下很小很小概率会有哈希冲突,

就算有了哈希冲突,java也不会就这么简单的认为两个元素是相等的。

所以从宏观上来看,从java方面来看,哈希即索引这个说法是行得通的。

 

其实这里是这样的,问题在于HashSet的add()。

我们可以追踪其源码,发现了以下片段

关于重写equals()和hashCode()的思考

关于重写equals()和hashCode()的思考

恍然大悟,我们可以看到HashSet添加元素时,调用的是HashMap的put(),

它不仅涉及了元素的哈希值,即hashCode(),还涉及到了equals()。

所以为什么上面只重写hashCode()之后HashSet还能添加s1和s2,就是因为equals()没有重写。

导致了虽然哈希值相同,但equals()不同,所以认为s1和s2是两个不同的元素。

 

所以综上所述,我们可以知道,对于这些哈希结构的东西,

它们判断元素重复是先判断哈希值然后再判断equals()的。

也就是说

先判断哈希值,如果哈希值相等,内容不一定等,此时继续判断equals()。

如果哈希值不等,那么此时内容一定不等,就不用再判断equals()了,直接操作。

 

4.两个都重写

例子

public class MyClass {

    public static void main(String[] args) {
        //Student对象
        Student s1 = new Student(\"jojo\", 18);
        Student s2 = new Student(\"jojo\", 18);

        //HashSet对象
        HashSet set = new HashSet();
        set.add(s1);
        set.add(s2);

        //输出两个对象
        System.out.println(s1);
        System.out.println(s2);

        //输出equals
        System.out.println(\"s1.equals(s2)的结果为:\" + s1.equals(s2));

        //输出哈希值
        System.out.println(\"哈希值为:s1->\" + s1.hashCode() + \"  s2->\" + s2.hashCode());

        //输出set
        System.out.println(set);
    }

    private static class Student {
        String name;
        int age;

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

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

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

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

输出结果

Student{name=\'jojo\', age=18}
Student{name=\'jojo\', age=18}
s1.equals(s2)的结果为:true
哈希值为:s1->101306313  s2->101306313
[Student{name=\'jojo\', age=18}]

结果分析

equals()返回 true

哈希值          一致

重写了两个方法后,equals()返回了true,哈希值也因为内容一样而一样,

更重要的是,HashSet只插入了一个元素。

 

6.最最最最后的总结

1.为什么要重写equals()

从正常角度而言,重写equals()是为了让两个内容一样的元素相等。

从java的哈希结构设计而言,哈希结构对元素的操作跟哈希值以及equals()有关,所以必须重写。

 

2.为什么要重写hashCode()

重写hashCode()是为了让哈希值跟内容产生关联,从而保证了哈希值跟内容一一对应,

提高哈希值的唯一性,减少哈希冲突。

:重写hashCode()不是必须的,只有跟哈希结构有关时才需要重写。

 

3.为什么重写equals()时必须重写hashCode()方法

其一

在跟哈希结构有关的情况下,判断元素重复是先判断哈希值再判断equals()。所以得两个都重写,

其二

重写了hashCode()减少了哈希冲突,就能直接判断元素的重复,而不用再继续判断equals(),从而提高了效率。 

起因
1.为什么要重写equals()
2.哈希值
3.哈希冲突
4.极其重要的一点
5.重写
   5.1 不重写equals不重写hashCode
   5.2 只重写equals不重写hashCode
   5.3 不重写equals只重写hashCode
   5.4 即重写equals也重写hashCode
6.最最最最后的总结

给TA打赏
共{{data.count}}人
人已打赏
随笔日记

彻底弄懂UTF-8、Unicode、宽字符、locale

2020-11-9 5:45:57

随笔日记

react-redux 的使用

2020-11-9 5:45:59

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索