相信你面试的时候,确定被问过 hashCode 和 equals 相关的问题 。如:java
好的,上面就是灵魂拷问环节。其实,这些问题仔细想一下也不难,主要是平时咱们不多去思考它。面试
下面就按照上边的问题顺序,一个一个剖析它。扒开 hashCode 的神秘面纱。算法
咱们一般说的 hashCode 其实就是一个通过哈希运算以后的整型值。而这个哈希运算的算法,在 Object 类中就是经过一个本地方法 hashCode() 来实现的(HashMap 中还会有一些其它的运算)。缓存
public native int hashCode();
能够看到它是一个本地方法。那么,想要了解这个方法究竟是用来干吗的,最直接有效的方法就是,去看它的源码注释。ide
下边我就用我蹩脚的英文翻译一下它的意思。。。函数
返回当前对象的一个哈希值。这个方法用于支持一些哈希表,例如 HashMap 。源码分析
一般来说,它有以下一些约定:性能
在实际状况下,Object 类的 hashCode 方法在不一样的对象中确实返回了不一样的哈希值。这一般是经过把对象的内部地址转换为一个整数来实现的。学习
ps: 这里说的内部地址就是指物理地址,也就是内存地址。须要注意的是,虽然 hashCode 值是依据它的内存地址而得来的。可是,不能说 hashCode 就表明对象的内存地址,实际上,hashCode 地址是存放在哈希表中的。this
上边的源码注释真可谓是句句珠玑,把 hashCode 方法解释的淋漓尽致。一下子我经过一个案例说明,就能明白我为何这样说了。
上文中提到了哈希表。什么是哈希表呢?咱们直接看百度百科的解释。
用一张图来表示它们的关系。
左边一列就是一些关键码(key),经过哈希函数,它们都会获得一个固定的值,分别对应右边一列的某个值。右边的这一列就能够认为是一张哈希表。
并且,咱们会发现,有可能有些 key 不一样,可是它们对应的哈希值倒是同样的,例如 aa,bb 都指向 1001 。可是,必定不会出现同一个 key 指向不一样的值。
这也很是好理解,由于哈希表就是用来查找 key 的哈希地址的。在 key 肯定的状况下,经过哈希函数计算出来的 哈希地址,必定也是肯定的。如图中的 cc 已经肯定在 1002 位置了,那么就不可能再占据 1003 位置。
思考一下,若是有另一个元素 ee 来了,它的哈希地址也落在 1002 位置,怎么办呢?
其实,上图就已经能够说明一些问题了。咱们经过一个 key 计算出它的 hashCode 值,就能够惟一肯定它在哈希表中的位置。这样,在查询时,就能够直接定位到当前元素,提升查询效率。
如今咱们假设有这样一个场景。咱们须要在内存中的一起区域存放 10000 个不一样的元素(以aa,bb,cc,dd 等为例)。那怎么实现不一样的元素插入,相同的元素覆盖呢?
咱们最容易想到的方法就是,每当存一个新元素时,就遍历一遍已经存在的元素,看有没有相同的。这样虽然也是能够实现的,可是,若是已经存在了 9000 个元素,你就须要去遍历一下这 9000 个元素。很明显,这样的效率是很是低下的。
咱们转换一种思路,仍是以上图为例。若来了一个新元素 ff,首先去计算它的 hashCode 值,得出为 1003 。发现此处尚未元素,则直接把这个新元素 ff 放到此位置。
而后,ee 来了,经过计算哈希值获得 1002 。此时,发现 1002 位置已经存在一个元素了。那么,经过 equals 方法比较它们是否相等,发现只有一个 dd 元素,很明显和 ee 不相等。那么,就把 ee 元素放到 dd 元素的后边(能够用链表形式存放)。
咱们会发现,当有新元素来的时候,先去计算它们的哈希值,再去肯定存放的位置,这样就能够减小比较的次数。如 ff 不须要比较, ee 只须要和 dd 比较一次。
当元素愈来愈多的时候,新元素也只须要和当前哈希值相同的位置上,已经存在的元素进行比较。而不须要和其余哈希值不一样的位置上的元素进行比较。这样就大大减小了元素的比较次数。
图中为了方便,画的哈希表比较小。如今假设,这个哈希表很是的大,例若有这么很是多个位置,从 1001 ~ 9999。那么,新元素插入的时候,有很大几率会插入到一个尚未元素存在的位置上,这样就不须要比较了,效率很是高。可是,咱们会发现这样也有一个弊端,就是哈希表所占的内存空间就会变大。所以,这是一个权衡的过程。
有心的同窗可能已经发现了。我去,上边的这个作法好熟悉啊。没错,它就是大名鼎鼎的 HashMap 底层实现的思想。对 HashMap 还不了解的,赶忙看这篇文章理一下思路。HashMap 底层实现原理及源码分析
因此,hashCode 有什么用。很明显,提升了查询,插入元素的效率呀。
这是万年不变,经久不衰的经典面试题了。让我油然想起,当初为了面试,背诵过的面经了,简直是一把心酸一把泪。如今还能记得这道题的标准答案:equals 比较的是内容, == 比较的是地址。
当时,真的就只是背答案,知其然而不知其因此然。再往下问,为何要重写 equals ,就懵逼了。
首先,咱们应该知道 equals 是定义在全部类的父类 Object 中的。
public boolean equals(Object obj) { return (this == obj); }
能够看到,它的默认实现,就是 == ,这是用来比较内存地址的。因此,若是一个对象的 equals 不重写的话,和 == 的效果是同样的。
咱们知道,当建立两个普通对象时,通常状况下,它们所对应的内存地址是不同的。例如,我定义一个 User 类。
public class User { private String name; private int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public User(String name, int age) { this.name = name; this.age = age; } public User() { } } public class TestHashCode { public static void main(String[] args) { User user1 = new User("zhangsan", 20); User user2 = new User("lisi", 18); System.out.println(user1 == user2); System.out.println(user1.equals(user2)); } } // 结果: false false
很明显,zhangsan 和 lisi 是两我的,两个不一样的对象。所以,它们所对应的内存地址不一样,并且内容也不相等。
注意,这里我尚未对 User 重写 equals,实际此时 equals 使用的是父类 Object 的方法,返回的确定是不相等的。所以,为了更好地说明问题,我仅把第二行代码修改以下:
//User user2 = new User("lisi", 18); User user2 = new User("zhangsan", 20);
让 user1 和 user2 的内容相同,都是 zhangsan,20岁。按咱们的理解,这虽然是两个对象,可是应该是指的同一我的,都是张三。可是,打印结果,以下:
这有悖于咱们的认知,明明是同一我的,为何 equals 返回的却不相等呢。所以,此时咱们就须要把 User 类中的 equals 方法重写,以达到咱们的目的。在 User 中添加以下代码(使用 idea 自动生成代码):
public class User { ... //省略已知代码 @Override public boolean equals(Object o) { //若两个对象的内存地址相同,则说明指向的是同一个对象,故内容必定相同。 if (this == o) return true; //类都不是同一个,更别谈相等了 if (o == null || getClass() != o.getClass()) return false; User user = (User) o; //比较两个对象中的全部属性,即name和age都必须相同,才可认为两个对象相等 return age == user.age && Objects.equals(name, user.name); } } //打印结果: false true
再次执行程序,咱们会发现此时 equals 返回 true ,这才是咱们想要的。
所以,当咱们使用自定义对象时。若是须要让两个对象的内容相同时,equals 返回 true,则须要重写 equals 方法。
在上边的案例中,其实咱们已经说明了为何要去重写 equals 。由于,在对象内容相同的状况下,咱们须要让对象相等。所以,不能用 Object 类的默认实现,只去比较内存地址,这样是不合理的。
那 hashCode 为何要重写呢? 这就涉及到集合,如 Map 和 Set (底层其实也是 Map)了。
咱们以 HashMap JDK1.8的源码来看,如 put 方法。
咱们会发现,代码中会屡次进行 hash 值的比较,只有当哈希值相等时,才会去比较 equals 方法。当 hashCode 和 equals 都相同时,才会覆盖元素。get 方法也是如此(先比较哈希值,再比较equals),
只有 hashCode 和 equals 都相等时,才认为是同一个元素,找到并返回此元素,不然返回 null。
这也对应 “hashCode 有什么用?”这一小节。 重写 equals 和 hashCode 的目的,就是为了方便哈希表这样的结构快速的查询和插入。若是不重写,则没法比较元素,甚至形成元素位置错乱。
答案是确定的。首先,在上边的 JDK 源码注释中第第二点,咱们就会发现这句说明。其次,咱们尝试重写 equals ,而不重写 hashCode 看会发生什么现象。
public class TestHashCode { public static void main(String[] args) { User user1 = new User("zhangsan", 20); User user2 = new User("zhangsan", 20); HashMap<User, Integer> map = new HashMap<>(); map.put(user1,90); System.out.println(map.get(user2)); } } // 打印结果: null
对于代码中的 user1 和 user2 两个对象来讲,咱们认为他是同一我的张三。定义一个 map ,key 存储 User 对象, value 存储他的学习成绩。
当把 user1 对象做为 key ,成绩 90 做为 value 存储到 map 中时,咱们确定但愿,用 key 为 user2 来取值时,获得的结果是 90 。可是,结果却大失所望,获得了 null 。
这是由于,咱们自定义的 User 类,虽然重写了 equals ,可是没有重写 hashCode 。当 user1 放到 map 中时,计算出来的哈希值和用 user2 去取值时计算的哈希值不相等。所以,equals 方法都没有比较的机会。认为他们是不一样的元素。然而,其实,咱们应该认为 user1 和 user2 是相同的元素的。
用图来讲明就是,user1 和 user2 存放在了 HashMap 中不一样的桶里边,致使查询不到目标元素。
所以,当咱们用自定义类来做为 HashMap 的 key 时,必需要重写 hashCode 和 equals 。不然,会获得咱们不想要的结果。
这也是为何,咱们平时都喜欢用 String 字符串来做为 key 的缘由。 由于, String 类默认就帮咱们实现了 equals 和 hashCode 方法的重写。以下,
// String.java 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; } public int hashCode() { int h = hash; if (h == 0 && value.length > 0) { char val[] = value; //把字符串中的每一个字符都取出来,参与运算 for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } //把计算出来的最终值,存放在hash变量中。 hash = h; } return h; }
重写 equals 时,可使用 idea 提供的自动代码,也能够本身手动实现。
public class User { ... //省略已知代码 @Override public int hashCode() { return Objects.hash(name, age); } } //此时,map.get(user2) 能够获得 90 的正确值
在重写了 hashCode 后,使用自定义对象做为 key 时,还须要注意一点,不要在使用过程当中,改变对象的内容,这样会致使 hashCode 值发生改变,一样得不到正确的结果。以下,
public class TestHashCode { public static void main(String[] args) { User user = new User("zhangsan", 20); HashMap<User, Integer> map = new HashMap<>(); map.put(user,90); System.out.println(map.get(user)); user.setAge(18); //把对象的年龄修改成18 System.out.println(map.get(user)); } } // 打印结果: // 90 // null
会发现,修改后,拿到的值是 null 。这也是,hashCode 源码注释中的第一点说明的,hashCode 值不变的前提是,对象的信息没有被修改。若被修改,则有可能致使 hashCode 值改变。
此时,有没有联想到其余一些问题。好比,为何 String 类要设计成不能够变的呢?这里用 String 做为 HashMap 的 key 时,能够算做一个缘由。你确定不但愿,放进去的时候还好好的,取出来的时候,却找不到元素了吧。
String 类内部会有一个变量(hash)来缓存字符串的 hashCode 值。只有字符串不可变,才能够保证哈希值不变。
很显然不是的。在 HashMap 的源码中,咱们就能看到,当 hashCode 相等时(产生哈希碰撞),还须要比较它们的 equals ,才能够肯定是不是同一个对象。所以,hashCode 相等时, equals 不必定相等 。
反过来,equals 相等的话, hashCode 必定相等吗? 那必须的。equals 都相等了,那说明在 HashMap 中认为它们是同一个元素,因此 hashCode 值必须也要保证相等。
结论:
关于最后这一点,就是 hashCode 源码注释中提到的第三点。当 equals 不等时,不用必须保证它们的 hashCode 也不相等。可是为了提升哈希表的效率,最好设计成不等。
由于,咱们既然知道它们不相等了,那么当 hashCode 设计成不等时。只要比较 hashCode 不相等,咱们就能够直接返回 null,而没必要再去比较 equals 了。这样,就减小了比较的次数,无疑提升了效率。
以上就是 hashCode 和 equals 相关的一些问题。相信已经能够解答你心中的疑惑了,也能够和面试官侃侃而谈。不再用担忧,面试官说换人了。