Java中的equals 和 hashCode ,是来自Object中的,我们比较熟悉的两个方法,至少,你问几乎任何一个JAVA程序员,无论有没有实际用到过这两个方法,都会说了解。我有的时候面试别人,也会问到这两个方法,但是,了解是一回事,究竟有多了解呢?今天面试,我就载在了这上面。然后我才发现,我所谓的了解,不过是人云亦云,跟别人吹NB罢了。这道题不会,犹如狠狠地抽了自己一个耳光。所以,定要弄个清楚。
好了,先说一下那哥们给的原题:对于一个类,如果我们按JDK的标准去重写equals和hashCode的话,那么,当这个类的两个实例的hashCode相等的时候,它们的equals方法是否相等?反之如何?
我当时的答案是,这两种情况下,它们都是相等的。然后,这哥们又看了看我的简历,就说让我等会,他去向领导汇报,然后,前台MM就过来通知我说,今天的面试结束了。。
很明显,我的答案是错误的,当时我前面已经答得不好了,所以在本来就不清楚它们的差别的情况下,答案肯定是有蒙的成份的,而刚好,这是很容易被发现的。当然,事情都过去了,再想也没有有,写这个的目的,是为了把这两个方法,完全地弄清楚。下面详细进行介绍:
1. 这两个方法最原始的实现
最简单直接的方式就是看源码,这两个方法在Object中的实现如下:
public boolean equals(Object obj) {
return (this == obj);
}
public native int hashCode();
可见一个是比较的地址,一个是本地方法,根据注释该方法应该是返回的根据对象的地址转化而成的一个对应的整数。这个实在看不出什么,好在这两个方法在注释中是有规范的,源码中的英文就不贴了,直接贴翻译后的中文:
equals
public boolean equals(Object obj)
指示其他某个对象是否与此对象“相等”。
equals 方法在非空对象引用上实现相等关系:
- 自反性:对于任何非空引用值 x,x.equals(x) 都应返回true。
- 对称性:对于任何非空引用值 x 和y,当且仅当y.equals(x) 返回true 时,x.equals(y) 才应返回true。
- 传递性:对于任何非空引用值 x、y和z,如果x.equals(y) 返回true,并且 y.equals(z) 返回true,那么x.equals(z) 应返回true。
- 一致性:对于任何非空引用值 x 和 y,多次调用x.equals(y) 始终返回true 或始终返回false,前提是对象上 equals比较中所用的信息没有被修改。
- 对于任何非空引用值 x,x.equals(null) 都应返回false。
Object 类的 equals 方法实现对象上差别可能性最大的相等关系;即,对于任何非空引用值 x 和y,当且仅当x 和y 引用同一个对象时,此方法才返回 true(x == y 具有值true)。
注意:当此方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码。
hashCode
public int hashCode()
返回该对象的哈希码值。支持此方法是为了提高哈希表(例如 java.util.Hashtable 提供的哈希表)的性能。
hashCode 的常规协定是:
- 在 Java 应用程序执行期间,在对同一对象多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是将对象进行equals
比较时所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。 - 如果根据equals(Object) 方法,两个对象是相等的,那么对这两个对象中的每个对象调用hashCode 方法都必须生成相同的整数结果。
- 如果根据 equals(java.lang.Object) 方法,两个对象不相等,那么对这两个对象中的任一对象上调用hashCode方法不 要求一定生成不同的整数结果。但是,程序员应该意识到,为不相等的对象生成不同整数结果可以提高哈希表的性能。
实际上,由 Object类定义的 hashCode 方法确实会针对不同的对象返回不同的整数。(这一般是通过将该对象的内部地址转换成一个整数来实现的,但是
JavaTM 编程语言不需要这种实现技巧。)
总结:上面的这部分内容,就是面试官所谓的JDK标准,也就是说,我们如果要重写这两个方法的话,是要按这个约定来重写的,而不是随便重写。简单的来说,重写后的equals,要具备自反性,对称性,传递性和一致性,而重写后的hashCode, 同样也具备一致性,同时,还有一个关键的协定,即如果根据equals(Object)方法,两个对象相等的话,则其hashCode也必然相等,而反之,则不然。换句话说,相同的对象,hashCode一定相同,而不同的对象,则hashCode可能相同,也可能不同。
如果认真看了这两段注释的话,那么今天那哥们的题的答案,也就是很明显的了,不但不会有错误,而且还可以给他把原理解释得一清二楚,所以,学习知识,最忌讳的,就是一知半解。
当然,如果我们定义的对象,在重写这两个方法时,没有遵守上面的原则,代码也不会编译失败,只是违背了准则,那么在用到这些方法,就会产生一些隐藏的BUG,所以这个规则是需要我们自觉遵守的,它不像范型,则编译器强制你遵守。我们可以看一下如String,Integer,Double等标准类,它们都对这两个方法有相应的重写,而且也是符合JDK标准的。
2. 使用场合
在了解了这两个方法的具体实现后,我们来看一下具体的应用。Java中有一个Set容器,其特点是里面的元素不能重复,那么,怎么判断里面的元素是否重复呢?我们还是看一下源码,可以看一下HashSet的具体实现:
其实HashSet是基于HashMap来实现的,由于HashMap是KEY-VALUE结构,所以HashSet只用了其中的KEY,而VALUE为一个静态的Object对象,个人认为只起到了点位的作用,至少我们使用HashSet时,不会用到。Set中有一个contains方法,该方法最终转化为对其中HashMap对象中的KEY进行查找。如下:
/** * Returns the entry associated with the specified key in the * HashMap. Returns null if the HashMap contains no mapping * for the key. */
final Entry<K,V> getEntry(Object key) {
int hash = (key == null) ? 0 : hash(key.hashCode());
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
从上面的代码我们可以看到,这个会根据对象的hashCode去再hash,之后在内部的一个数组中,根据hash的结果去查找相应的对象,当然不同的hashCode再hash之后的值可能是一样的,这就是所谓的hash冲突,这样的话,系统采用链表的形式存储冲突的对象。不过这个不是要说的重点。
重点是:能找到的前提,是能找到一个对象的hash值和要查找的对象完全一样,但这个还不保险,在这个前提下,还要继续判断这个对象的地址是否和要查找的相等,若不等,则看它们的equals方法是否相等。这两者满足其一的话,说明能找到要查找的对象,也就是说,Set中已经有了该元素。
从上面的描述我们可以知道,最终判断两个对象是否相等的充分必要条件是,两者的地址相同,或者equals方法比较的结果为true.
那按这个说法的话,我们只需要equals方法足矣,为啥还要实现hashCode呢?这是为了提高效率,如果两个对象的不等,通常情况下,其hashCode值也是不一样的,那么就可以利用hashCode先进行过滤。即根据前面约束的推论,如果两个对象的hashCode不同,则这两个对象一定不等。
另外,根据HashSet的查找逻辑,如果我们自定义的对象只实现了equals,没有重写hashCode的话,那这样也是有问题的。假定我们重写了equals,认为只要两个对象的id属性值一样,这两个对象就一样的话,那么,由于没有重写hashCode,所以hashCode的值的生成,还是和两个对象的地址相关,很明显这两个对象的地址是不一样的,那这样的话,两个对象的hashCode值自然也不一样,HashSet则永远会将它们看做是不同的对象。
写了这么多,我个人认为是理解了这两个方法的区别和应用场合了,相信下次再遇到类似的问题以及变体,我都不会再答错了,如果面试别人的时候,我也会更细致的,考察一下他们对这两个方法的理解。