equals和hashCode的关系

要搞清楚题目中的问题就必须搞明白equals方法和hashCode方法分别是什么,和诞生的原因,当搞明白了这一点其实题目就不算是个问题了,下面我们来探讨分别探讨一下两者代表的意义。

hashCode

笔者看到很多地方都对hashCode有两个误解

  • 对象默认的hashCode是对象的地址。
  • 默认的equals会先比较对象的hashCode,如果hashCode相同则代表两个对象是同一个对象。

在这里笔者先给出这两个问题的结论,后面会给出证明。

  1. hashCode并不是对象的地址。
  2. 默认的equals比较的是对象的地址,与hashCode无关。

事实上想求证hashCode是不是对象的地址这件事情说容易也容易,说难也难。其实笔者在网上有很多不知出处与权威性的文章都写hashCode就是对象的地址,从这点上来说,想找到真实答案也挺不容易的,“谎言重复千遍便是真理”说的大概就是这个意思。之所以说容易是因为只要通过阅读Oracle的JavaAPI注释便可知道正确答案,所以其实学习一个东西最好的办法还是看官方的文档。但因为Oracle的API是英文的,对母语不是英文的我们来说或许会有些痛苦,即时你能看懂英文文档,为了容易我们也可能选择找中文的文章来看,不幸的是大多数软件的文档没有中文的。

OracleAPI中对hashCode()的注释如下:

  1. Returns a hash code value for the object. This method is supported for the benefit of hash tables such as those provided by HashMap.
  2. As much as is reasonably practical, the hashCode method defined by class Object does return distinct integers for distinct objects. (This is typically implemented by converting the internal address of the object into an integer, but this implementation technique is not required by the JavaTM programming language.)

第2句话的意思是说,不同的Object对象的返回不同的hashCode,这通常通过将对象地址进行某种转换映射为一个integer,但并不限制具体的实现方法。换句话说,hashCode的生成策略是由jdk的实现决定的。这已经能够说明hashCode并不等于对象的物理地址,虽然实现方式与其有关,但绝不意味着相等。其实通过下面的代码我们也可以从某种程度上推测证明两者并不严格相等。

public class B {
public static void main(String[] args) {
B b1 = new B();
B b2 = new B();
System.out.println(b1.hashCode());
System.out.println(b2.hashCode());
}
}
> 356573597
> 1735600054

这个代码非常简单,从一开始启动虚拟机到b1和b2的内存分配之间并没有任何其他的过多干扰,换句话说,堆内存的空闲是很多的,并不存在内存分配中的指针碰撞或者需要维护不连续的内存空闲列表,因此b1和b2的内存分配是相当连续的。如果hashCode代表着内存地址,那么两者应该相差不大,但事实上两者看不出任何内存分布上的联系。

在来解释第一句话,这句话的意思是说一些基于hash的数据结构如HashMap等会受益于此方法,这就可以做出推测,hashCode的出现是为一些基于hash的数据结构服务的。后面我们会分析HashMap是如何根据hashCode去提升性能的,这里必须提到JVM的一个细节:java对象在内存分配之后,hashCode存在于对象头中,但这个值并不是内存分配完成之后就有的,当第一次调用对象的hashCode方法,对象的hashCode值就会存放在对象头中。

至此,关于hashCode的第一个误解已经解决了,下面我们证明第二个,来看下面的代码。

public class B {
@Override
public int hashCode() {
return 1;
}
public static void main(String[] args) {
B b1 = new B();
B b2 = new B();
System.out.println(b1.equals(b2));
System.out.println(b1 == b2);
}
}
> false
> false

如上所示,b1和b2拥有相同的hashCode,但是不管是equals还是==比较,都返回了false,这至少证明了Object的equals方法与hashCode并无任何关联,查看Object的equals方法源码便知。

public class Object {
public boolean equals(Object obj) {
return (this == obj);
}
}

equals

equals比hashCode好理解的多,它的设计初衷是为了让编程人员自己定义两个对象是否相等,这与地址无关。因为对于java虚拟机来讲,只有两个引用指向同一个对象,两个对象才能看作是相等的。当然,这个原因也不是笔者凭空猜测的,OracleAPI中有这两句话如下:

public boolean equals(Object obj)

Indicates whether some other object is "equal to" this one.

The equals method for class Object implements the most discriminating possible equivalence relation on objects; that is, for any non-null reference values x and y, this method returns true if and only if x and y refer to the same object (x == y has the value true).

但其实下面还有一句话如下:

Note that it is generally necessary to override the hashCode method whenever this method is overridden, so as to maintain the general contract for the hashCode method, which states that equal objects must have equal hash codes.

这句话告诉我们当一个对象的hashCode方法被重写的时候,为了保持hashCode的常规协定,建议重写hashCode方法,这里所指的hashCode常规协定如下:

If two objects are equal according to the equals(Object) method, then calling the hashCode method on each of the two objects must produce the same integer result.

这条contract告诉我们,如果两个对象equals,则他们要有相同的hashCode,这并不是必须满足的条件,事实上我们很可能经常不遵守这个协定,比如下面的代码:

public class B {
@Override
public boolean equals(Object obj) {
return true;
}
public static void main(String[] args) {
B b1 = new B();
B b2 = new B();
System.out.println(b1.equals(b2));
System.out.println(b1.hashCode() == b2.hashCode());
}
}

既然这个协定不是必须要遵守的,为什么Java建议我们如果重写了equals方法就要重写hashCode方法,还告诉我们如果两个对象equals要有相同的hashCode呢?

equals & hashCode & HashSet

前文提到,Java中的hashCode主要是为了一些使用hash的数据结构而存在的。这里以HashSet举例,Set中是不允许对象有重复的,这里的重复就是相等的元素,注意:这里要分是两个元素是物理地址上的相等,还是通过equals比较的相等。从逻辑上来说,如果两个元素被用户定义了的equals方法比较的结果为true,那么不管两个对象hashCode值是否相等,它们能应该被定义为“重复”,但事实上如果不重写hashCode,两个equals的对象输出的hashCode不同,它仍然被当作不同的元素被HashSet, HashMap等一系列hash的数据结构对待。

public class B {
@Override
public boolean equals(Object obj) {
return true;
}
public static void main(String[] args) {
B b1 = new B();
B b2 = new B();
HashSet<Object> set = new HashSet();
set.add(b1);
set.add(b2);
System.out.println(set.size());
}
}
> 2

以上代码set中的元素为两个,尽管b1.equals(b2) = true这在逻辑上就与元素存放不同对象相违背了(这里以HashSet举例,实际上任何类似的使用hash的数据结构都可以如此推导),因此Java告诉我们,如果重写了equals方法,请务必重写hashCode方法,使得两个equals的对象拥有相同的hashCode,可以被hash的集合类当作相同的元素看待。

我们顺便来看一下set.add(E e)方法的内容:

public boolean add(E e) {
return map.put(e, PRESENT)==null;
}

这里调用了HashMap的put方法(HashSet就是用HashMap实现的),我们继续跟进去:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 先比较hash, 在比较地址,最后调用equals
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
....
}
if (e != null) { // existing mapping for key
....
}
}
...
}

需要说明的是这里的hash并不是对象的hashCode,而是通过下面的方式处理后的结果

static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

但由于相同的输入有相同的输出,这里姑且把hash当作hashCode处理,当hashSet调用add方法时,会判断HashMap中hash对应的bucket中是否有元素,如果有,判断两个元素的hash值是否相同,如果不同,HashMap会直接当作不同(逻辑上的)的元素处理,如果相同,还会比较equals和地址是否相同来判定该对象是否真的相同。

逻辑听起来好像有点绕,简单来说就是HashMap认为,如果两个对象hashCode不同,那这两个对象就不相等,如果hashCode相同,则根据地址和equals判定。

综上所述,正式因为这些基于hash的数据结构,才使得我们在重写equals时要重写hashCode,否则在这些集合类中关于两个对象是否相等的判定会在语义上变得不严谨,除此之外,equals和hashCode再无任何关联。

为什么重写equals后要重写hashCode的更多相关文章

  1. java 中为什么重写 equals 后需要重写 hashCode

    本文为博主原创,未经允许不得转载: 1. equals 和 hashCode 方法之间的关系 这两个方法都是 Object 的方法,意味着 若一个对象在没有重写 这两个方法时,都会默认采用 Objec ...

  2. 讲解:为什么重写equals时必须重写hashCode方法

    一 :string类型的==和equals的区别: 结论:"=="是判断两个字符串的内存地址是否相等,equals是比较两个字符串的值是否相等,具体就不做扩展了,有兴趣的同学可以去 ...

  3. 为什么重写equals一定要重写hashCode?

    大家都知道,equals和hashcode是java.lang.Object类的两个重要的方法,在实际应用中常常需要重写这两个方法,但至于为什么重写这两个方法很多人都搞不明白,以下是我的一些个人理解. ...

  4. 为什么重写equals一定要重写hashCode方法?

    大家都知道,equals和hashcode是java.lang.Object类的两个重要的方法,在实际应用中常常需要重写这两个方法,但至于为什么重写这两个方法很多人都搞不明白. 下面我们看下Objec ...

  5. 为什么重写equals时必须重写hashCode方法?(转发+整理)

    为什么重写equals时必须重写hashCode方法? 原文地址:http://www.cnblogs.com/shenliang123/archive/2012/04/16/2452206.html ...

  6. 为什么重写 equals 方法 必须重写 hashCode

    自己学到这,就记录了下来,代码都是自己敲得,有不对的地方希望大神指点出来 为什么重写 equals 方法 必须重写 hashCode 如果你重写了equals,比如说是基于对象的内容实现的,而不重写 ...

  7. java中为什么重写equals时必须重写hashCode方法?

    在上一篇博文Java中equals和==的区别中介绍了Object类的equals方法,并且也介绍了我们可在重写equals方法,本章我们来说一下为什么重写equals方法的时候也要重写hashCod ...

  8. 编写高质量代码改善C#程序的157个建议——建议12: 重写Equals时也要重写GetHashCode

    建议12: 重写Equals时也要重写GetHashCode 除非考虑到自定义类型会被用作基于散列的集合的键值:否则,不建议重写Equals方法,因为这会带来一系列的问题. 如果编译上一个建议中的Pe ...

  9. 为什么重写equals时必须重写hashCode方法?

    原文地址:http://www.cnblogs.com/shenliang123/archive/2012/04/16/2452206.html 首先我们先来看下String类的源码:可以发现Stri ...

随机推荐

  1. Linux删除(清空)正在运行的应用日志文件内容 及 查看服务器剩余空间

    在测试环境定位问题时,如果发现日志文件内容太多或太大,有时需要删除该日志,如Tomcat,Nginx日志.以前每次都是先rm -rf ***.log,然后重启应用.直到后来发现了以下命令,原来可以不用 ...

  2. 2018-2019-2 网络对抗技术 20165332 Exp2 后门原理与实践

    2018-2019-2 网络对抗技术 20165332 Exp2 后门原理与实践 - 实验内容 任务一:使用netcat获取主机操作Shell,cron启动 任务二:使用socat获取主机操作Shel ...

  3. vue 报错 Cannot read property '__ob__' of undefined的解决方法

    记不清第n次遇到这个错误了,但是脑子就是不好用,记不住解决办法啊,每次都要找好久才能找到错误,网上还一篇篇的全是错误答案......所以写篇随笔,记录下,方便大家也方便我自己. 网上有人说是组件循环了 ...

  4. 兼容360模式自动播放视频【需要flvpalyer.swf】

    <object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" codebase="http://down ...

  5. IOS UIApplication和AppDelegate 关系

    UIApplication.AppDelegate.委托等的关系?  什么是委托?为什么要有委托?委托在Iphone中的实现机制是怎样的? 一般来说,我们创建了一个Iphone项目,默认会有这个mai ...

  6. oracle增加sequence

    (1)删除序列;  (2)重新创建: 这个方法比较简单粗暴. drop sequence  sequence_name; create sequence   sequence_name minvalu ...

  7. COM 学习

    一.COM (Component Object Model) 二.COM+ (Component Services) 三.DCOM (Distributed Component Object Mode ...

  8. JS 取出DataGrid 列

    var dt = document.all.<%= dgList.ClientID %>//找到你的grid在客户端的table for(var i = 1; i < dt.rows ...

  9. java基础---->Zip压缩的使用

    java中提供了对压缩格式的数据流的读写.它们封装到现成的IO 类中,以提供压缩功能.下面我们开始java中压缩文件的使用. 目录导航: 关于压缩的简要说明 GZIP压缩文件的使用 ZIP压缩文件的使 ...

  10. addpath

    这个命令见得很多了,一直懒得理他,自己直接加绝对路径.但是,这个破命令出现太多,我改得都掉脾气,写写. 1.  添加路径:addpath('当前路径中的文件夹名1','当前路径下的文件夹名2','当前 ...