前言相信大家在面试的时候一定被问过关于hashCode和equals的问题。如:什么是hashCode?它是怎么来的?有什么用?经典问题,equals和==有什么区别?为什么要重写equals和hashCode?重写equals后是否需要重写hashCode?为什么?hashCode相等时,equals一定相等吗?反之亦然?好了,以上就是灵魂拷问环节。其实,仔细思考这些问题并不难,主要是我们很少去思考。在正文下面,按照上面问题的顺序,一一分析。揭开哈希码的神秘面纱。什么是哈希码?我们平时所说的hashCode,其实就是经过哈希处理后的一个整数值。这个hash操作的算法是在Object类中通过一个本地方法hashCode()实现的(HashMap中还会有一些其他的操作)。publicnativeinthashCode();你可以看到它是一个本地方法。那么,要想知道这个方法是干什么用的,最直接有效的方法就是阅读它的源码注释。下面我就用我蹩脚的英文来翻译一下它的意思。..返回当前对象的散列。该方法用于支持一些哈希表,如HashMap。一般来说,它有如下约定:如果对象的信息没有被修改,那么,在程序执行过程中,对于同一个对象,无论调用多少次hashCode方法,都应该是相同的值回。当然,在同一程序的不同执行过程中,结果不需要保持一致。如果两个对象的equals方法返回相同的值,那么调用它们各自的hashCode方法也必须返回相同的结果。(ps:这句话回答了上面的部分问题,后面会用例子来证明这一点)当两个对象的equals方法的返回值不一样的时候,那么他们的hashCode方法就不一定要保证了必须返回不同的值。但是,我们应该知道,在这种情况下,我们最好也将hashCode设计为返回不同的值。因为,这样做有助于提高哈希表的性能。现实中,Object类的hashCode方法确实会在不同的对象中返回不同的哈希值。这通常是通过将对象的内部地址转换为整数来完成的。ps:这里所说的内部地址是指物理地址,即内存地址。需要注意的是,虽然hashCode值是根据其内存地址获取的。但是,不能说hashCode代表对象的内存地址。其实hashCode地址是存放在哈希表中的。上面的源码注释真的是一句话,把hashCode方法解释的很形象。过段时间再通过一个案例来说明,我就会明白我为什么这么说了。什么是哈希表?上面提到了哈希表。什么是哈希表?直接看百度百科的解释吧。用一张图来说明他们的关系。左栏是一些键码(键)。通过hash函数,他们都会得到一个固定的值,对应右列的某个值。右边的列可以认为是哈希表。而且,我们会发现有些key可能不同,但是它们对应的hash值是相同的,比如aa和bb都指向1001。但是同一个key一定不能指向不同的value。这个也很好理解,因为哈希表就是用来找key的哈希地址的。当密钥确定后,哈希函数计算出的哈希地址也必须确定。如图,cc已经确定在1002位置,所以不可能占据1003位置。想一想,如果又来一个元素ee,它的hash地址也落在1002位置,那怎么办?hashCode有什么用?其实上图已经可以说明一些问题了。我们可以通过一个key计算出它的hashCode值来唯一确定它在哈希表中的位置。这样在查询的时候,可以直接定位到当前元素,提高查询效率。现在让我们假设这样一个场景。我们需要在一个内存区域存储10000个不同的元素(以aa、bb、cc、dd等为例)。那么如何插入不同的元素并覆盖相同的元素呢?我们能想到的最简单的方法就是每存储一个新元素就遍历已有的元素,看看有没有相似的。虽然这样也是可以实现的,但是如果已经有9000个元素,还需要遍历这9000个元素。显然,这样的效率是很低的。我们换一种思路,还是以上图为例。如果有新元素ff来了,先计算它的hashCode值,为1003,如果这里没有元素,就直接把新元素ff放在这个位置。然后,ee来了,通过计算hash值得到1002。此时发现1002位置已经存在一个元素,然后通过equals方法比较是否相等,发现只有一个dd元素,显然不等于ee。然后,将ee元素放在dd元素后面(可以用链表的形式存储)。我们会发现,当有新的元素到来时,先计算它们的哈希值,然后再确定存储位置,这样可以减少比较的次数。比如ff不需要比较,ee只需要和dd比较一次。当元素越来越多时,只需要将新元素与当前哈希值相同位置的已有元素进行比较即可。不需要比较不同哈希值位置的元素。这大大减少了元素比较的次数。为了方便,图中画的哈希表比较小。现在假设这个哈希表很大,比如有很多个位置,从1001到9999。那么,当插入一个新的元素时,很有可能会插入到一个没有元素存在的位置,这样就不用比较了,效率很高。但是,我们会发现这样也有一个缺点,就是哈希表占用的内存空间会变大。所以这是一个权衡。感兴趣的同学可能已经发现了。不说了,上面的方法很熟悉。没错,这就是著名的HashMap实现背后的思想。如果你还不了解HashMap,请阅读这篇文章了解一下:HashMap底层实现原理及源码分析那么,hashCode有什么用呢?显然,查询和插入元素的效率都得到了提升。等于和==有什么区别?这是一道千古不变的经典面试题。想起面试时背诵面试经,心里难受得泪流满面。我还记得这个问题的标准答案:equals比较内容,==比较地址。当时真的只是背诵答案,知其然不知其所以然。进一步问,为什么重写等于,我很困惑。首先我们要知道equals是在所有类的父类Object中定义的。publicbooleanequals(Objectobj){return(this==obj);}可以看到它的默认实现是==,用来比较内存地址。所以,如果一个对象的equals没有被重写,它和==的效果是一样的。我们知道,当创建两个普通对象时,一般情况下,它们对应的内存地址是不同的。比如我定义了一个User类。publicclassUser{privateStringname;privateintage;publicStringgetName(){returnname;}publicvoidsetName(Stringname){this.name=name;}publicintgetAge(){returnage;}publicvoidsetAge(intage){this.age=age;}publicUser(Stringname,intage){this.name=name;this.age=age;}publicUser(){}}publicclassTestHashCode{publicstaticvoidmain(String[]args){Useruser1=newUser("zhangsan",20);Useruser2=newUser("lisi",18);System.out.println(user1==user2);System.out.println(user1.equals(user2));}}//result:falsefalse很明显,张三和李四是两个人,两个不同的对象。因此,它们对应不同的内存地址,它们的内容也不相等。请注意,我在这里没有为User重写equals。其实equals此时使用的是父类Object的方法,返回的肯定不是equal。因此,为了更好的说明问题,我只修改第二行代码如下://Useruser2=newUser("lisi",18);Useruser2=newUser("张三",20);让user1和user2有相同的内容,都是张三,20岁。按照我们的理解,这虽然是两个对象,但应该指的是同一个人,都是张三。然而打印结果如下:这与我们的认知相反,明明是同一个人,为什么equals返回的是unequal。所以,这时候我们就需要重写User类中的equals方法来达到我们的目的。在User中添加如下代码(使用idea自动生成代码):publicclassUser{...//省略已知代码@Overridepublicbooleanequals(Objecto){//如果两个对象的内存地址相同,则表示指向同一个对象,所以内容一定是一样的。if(this==o)returntrue;//类不一样,更不会相等if(o==null||getClass()!=o.getClass())returnfalse;Useruser=(User)o;//比较两个对象中的所有属性,即name和age必须相同才能认为两个对象相等return==user.age&&Objects.equals(name,user.name);}}//打印结果:falsetrue再次执行程序,我们会发现此时equals返回true,这就是我们想要的。所以当我们使用自定义对象时。如果需要equals在两个对象的内容相同时返回true,则需要重写equals方法。为什么要重写equals和hashCode?在上面的案例中,我们其实已经解释了为什么要重写equals。因为,在对象内容相同的情况下,我们需要让对象相等。所以不使用Object类的默认实现,只比较内存地址是不合理的。为什么要重写hashCode?这就涉及到了集合,比如Map、Set(底层其实就是一个Map)。我们看一下HashMapJDK1.8的源码,比如put方法。我们会发现代码中会多次比较hash值,只有当hash值相等时,才会比较equals方法。只有当hashCode和equals相同时,元素才会被覆盖。get方法也是如此(先比较hash值,再比较equals),只有当hashCode和equals相等时,才认为是同一个元素,找到并返回这个元素,否则返回null。这也对应了《hashCode有什么用?部分。重写equals和hashCode的目的是为了方便哈希表等结构的快速查询和插入。如果不改写,元素就无法比较,甚至元素的位置也会乱。重写equals后是否需要重写hashCode?答案是肯定的。首先,在上面JDK源码注释的第二点中,我们会发现这条语句。其次,让我们尝试在不覆盖hashCode的情况下覆盖equals,看看会发生什么。publicclassTestHashCode{publicstaticvoidmain(String[]args){Useruser1=newUser("zhangsan",20);Useruser2=newUser("zhangsan",20);HashMap
