彻底搞懂equals以及hashCode方法(源码级分析)

在开发过程中,我们经常会遇到要重写equals方法和重写hashCode方法的情况,那么,我们为什么要重写这个两个方法呢?重写这个两个方法有什么实际的作用吗?先别急,我们一一来分析,先看equals方法。

一、equals方法

我们先从超类Object里截取equals方法:

注意看,在Object类中,如果直接调用equals方法的话,是用的“==”来比较的,也就是说,它是默认比较的两个对象的内存地址值。理解了这一点,我们来看以下代码:

public class Test {

    public static void main(String args[]) {
        String s1 = new String("1111");
        String s2 = new String("1111");

        boolean result = s1.equals(s2);
        System.out.println(result); // 输出结果为:true

    }
}

我们都知道,第四行和第五行代码的意思是在堆内存中创建了两个对象,因此,s1和s2两个变量保存的是指向这个两个对象的内存地址,输入两个对象的内容是一样的,但是它们的内存地址值是不一样的。如下图所示:

我们知道任何类都是默认继承Object类的,String类当然也不例外,从上述的输出结果推断,如果String没有重写equals方法的话,那么s1和s2比较的一定是两者的内存地址,显然是不相等的,应该为false。但实际的输出结果为true,因此,我们可以肯定的是String类中一定重写了equals方法,我们点进去看一下String的源码:  

果然,如果重写了equals方法,那么按照类继承的规则,String类调用的是自身的equals,s1跟s2两者比较的规则则是按照String类重写后的equals方法来比较,很显然,String类的比较规则是按照值来比较的,因此结果会输出true。

为了验证我们的想法,那我们试着自定义一个Student类(注意:是没有重写equals方法的

public class Student {

    String username;
    String password;

    public Student() {
    }

    public Student(String username, String password) {
        this.username = username;
        this.password = password;
    }
}

测试:

public static void main(String args[]) {
    Student s1 = new Student("张三","123");
    Student s2 = new Student("张三","123");

    boolean result = s1.equals(s2);
    System.out.println(result);
}

输出结果为:false

这是为啥呢?因为,Student类默认是继承Object的,而Student没有重写equals方法,那么默认就会调用父类也就是Object类的equals方法,而Object的equals是比较两者之间的内存地址,s1和s2两个对象都是new出来的,内存地址肯定是不一样的,因此结果为false。如果我们在student类中重写了equals方法,那么输出的结果就变成了true了。

二、hashCode方法

我们先看一个现象:在做开发的时候,定义一个实体类,往往需要重写hashCode方法以及equals方法。而且不管是我们自己定义的类,sun公司定义的类诸如String、Integer等等都重写了equals方法,以及hashCode方法,那么这时为什么呢?

先说结论:这是为了提高存储载体的查询效率。举个例子:如果你存储的载体底层是采用了哈希表的数据结构。例如set集合,那么你将一个重写了hashCode方法的元素存进去后,再通过查询获取到这个元素的效率会很高。这是为什么呢?要理解这个,我们得先知道什么是哈希表?哈希算法又是什么鬼?

2.1、哈希表

散列表[Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。(摘抄自百度百科)

以上是比较官方的定义,相信没有多少人能看懂,其实我开始也看不太懂。事实上,哈希表的构成其实很简单。之前我们用过的数组已经链表相信都知道,数组的查询效率较高,但增删效率较低,链表的查询效率较低,但增删效率较高。可能你没有一个概念,因为你平常遇到的场景都是数据量比较小的,如果遇到那种千万级别的数据量,底层数据结构的选取对于系统性能的提升是很明显的。

哈希表其实本质上就是一个数组跟链表的结合体,它综合了两者之间的优点,它的查询效率比链表高,增删效率比数组高,相当于是一个折中的方案。它大概长这个德行:

 

一个数组,然后每个数组项下挂一个单向链表

2.2、set集合的存储原理

(这里我们以set集合为例,因为它底层是通过哈希表来实现的)我们来看以下代码:

public class Test {
    public static void main(String args[]) {
        Map<Integer,String> map = new HashMap<>();
        map.put(1, "张三");
        map.put(2, "李四");
        map.put(2,"王五");

        System.out.println(map.size()); // 2

        String s1 = map.get(1);
        String s2 = map.get(2);
        System.out.println(s1); // 张三
        System.out.println(s2); // 王五
    }
}

map.put()实现原理

第一步,先将key和value封装到一个节点对象中

第二步:底层会调用key的hashCode方法得到哈希值,然后通过哈希算法,将哈希值转换成数组的下标,如果下标位置上没有任何元素,就把节点对象添加到这个位置上。如果说下标的位置上对应有链表,那么就会调用节点key的equals方法去与链表上每一个节点的key进行比较,如果所有的equals方法的方法结果都是false,那么这个新节点就将被添加到链表的末尾,如果有其中一个返回true,那么这个节点就会被覆盖掉。这就是了为什么set集合是不可重复的底层原因。

map.get()实现原理

先调用key的hashCode方法得出哈希值,再通过哈希算法转换成数组下标,通过数组下标快速的定位到数组的某一个位置上。如果这个位置上怎么也没有,那么会拿着参数key和单向链表的key进行equals,如果equals方法返回的是false,代表没有搜索到,此时get方法返回的就是null,如果equals方法返回的是true,代表搜索到了,此时get方法获取到的就是key对应的value值。

分析

上面的map集合,key是Integer类型,value是String类型两者其实都重写了hashCode方法和equals方法,按照以上过程存储之后的哈希表结构如下:

 如果再存储一个新的数据赵六,按照上述步骤,那么就会挂载到李四节点的下面,形成一个单向链表

回到原点,如果我们要存储的元素没有重写hashCode方法和equals方法,那么根据上述的存储过程,元素会调用父类的equals方法以及hashCode方法。如下代码:

public class HashMapTest02 {
    public static void main(String[] args) {
        // Student类没有重写hashCode方法
        Student s1 = new Student("张三");
        Student s2 = new Student("张三");

        Set<Student> students = new HashSet<>();
        students.add(s1);
        students.add(s2);
        System.out.println(students.size()); // 输出2
        
        System.out.println("s1的哈希码为:" + s1.hashCode()); // 输出:356573597
        System.out.println("s2的哈希码为:" + s2.hashCode());// 输出:1735600054
    }
}

输出的结果竟然是2

仔细分析过程:首先,s1变量和s2变量两者存储的是对象的内存地址,因此调用hashCode方法得到的哈希码肯定是不一致的。那么由哈希码通过哈希算法得出的数组下标则不一致,因此,两个元素都会被存储到哈希表中。如下图所示:

 

因此,set集合中会存储两个相同的值,这也是为什么集合的长度为2的原因。

三、总结

在实际开发过程中,我们之所以要重写hashCode方法,就是为了使底层为哈希表的存储载体如集合等的存储查询效率更高。我们最常用的存储载体就是集合了,看完上面的分析过程,对集合的一些总结概要相信你一个很容易理解了:

1、为什么HashMap集合是无序不可重复的?

因为哈希算法是随机的,因此无法确定算出的数组下标是否是同一个,即无法确定挂载到哪个单向链表上。 equals方法来保证HashMap集合的key不可重复。如果key重复了,value会覆盖掉。

2、哈希表HashMap使用不当时无法发挥性能

假设将所有的hashCode()方法返回值固定为某个值,那么会导致底层哈希表变成了 纯单向链表。这种情况我们称之为:散列分布不均匀。

什么是散列分布均匀? 假设有100个元素,10个单向链表,那么每个单向链表上有10个节点,这是最好的,是散列分布均匀的。

假设将所有的hashCode()方法返回值都设定为不一样的值,可以吗,有什么问题?不行,因为这样的话导致底层哈希表就成为一维数组了,没有链表的概念了。也是散列分布不均匀。因此散列分布均匀需要你重写hashCode()方法时有一定的技巧。

3、放在HashMap集合key部分的元素,以及放在HashSet集合中的元素,需要同时重写hashCode和equals方法。

4、放在HashMap集合key部分的元素,以及放在HashSet集合中的元素,需要同时重写hashCode和equals方法。

5、HashMap集合的默认初始化容量是16,默认加载因子是0.75,这个默认加载因子是当HashMap集合底层数组的容量达到75%的时候,数组开始扩容。

6、HashMap集合初始化容量必须是2的倍数,这也是官方推荐的,这是因为达到散列均匀,为了提高HashMap集合的存取效率,所必须的。

7、向Map集合中存,以及从Map集合中取,都是先调用key的hashCode方法,然后再调用equals方法!equals方法有可能调用,也有可能不调用。

拿put(k,v)举例,什么时候equals不会调用?k.hashCode()方法返回哈希值,哈希值经过哈希算法转换成数组下标。数组下标位置上如果是null,equals不需要执行。拿get(k)举例,什么时候equals不会调用?k.hashCode()方法返回哈希值,哈希值经过哈希算法转换成数组下标。数组下标位置上如果是null,equals不需要执行。

8、哈希碰撞

对于哈希表数据结构来说:如果o1和o2两个对象的hash值相同,一定是放到同一个单向链表上。当然如果o1和o2的hash值不同,但由于哈希算法执行结束之后转换的数组下标可能相同,此时会发生“哈希碰撞”。但这种概率及其微小。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值