前面两节内容咱们详细讲解了Hashtable算法和源码分析,针对散列函数始终逃脱不掉hashCode的计算,本节咱们将详细分析hashCode和equals,同时您将会看到本节内容是从《Effective Java》学习整理而来(吐槽一句,这本书中文版翻译的真垃圾),对于《Effective Java》这本书颇有学习价值,可是我不会像其余童鞋同样,直接从这本书讲解一个系列,我所采用的是学习到对应地方而后参考不一样java经典书籍进行总结,按部就班式这样效果更佳,好了,咱们开始吧。java
翻看《Effective Java》关于equals这一节内容,直接抛出重写equals必须遵照的以下五大约定,当我看到这几大特性时,顿时惊呆了,这不就是大学线代讲解矩阵时的特色么,学以至用原来是这么个道理。算法
一、自反性:对于非空的对象x,x.equals(x)必须返回true.ide
二、对称性:对于非空的对象x和y,若x.equals(y)等于true时,那么y.equals(x)也必须返回true.函数
三、传递性:对于非空的对象x、y和z,若是x.equals(y)和y.equals(z)等于true时,那么x.equals(z)也必须返回true源码分析
四、一致性:对于非空的对象x和y,若是利用equals判断对象的信息没有被修改时,不管调用多少次,那么x.equals(y)要么为true,要么为false性能
五、对于非空的对象x,x.equals(null)必须返回false学习
关于第一点很好理解,非空对象自身引用必须相等,对于第二点书中所给的例子则是将重写对象比较某个字符串时不区分大小写,可是字符串对象是区分大小写,如此这样将致使对称不一致问题,对于第三点则是继承时注意equals的传递性,第4点则强调屡次调用经过equals判断的恒等性,最后一点更好理解如若不判断则会抛出空指针异常。那么咱们实际在重写equals时可将如下几点做为模板来使用就能够啦。this
一、使用“==”判断两个对象是否引用相同spa
二、使用instanceof操做符来检查参数类型是否相同翻译
三、若类型相同,则将参数转换为正确的类型
四、比较对象中每一个值是否都相等,若所有相等则返回true,不然为false
如上几点模板来自《Effective Java》对重写equals的总结,固然咱们能够从重写字符串对象中的equals找到如上影子,字符串对象的equals方法以下:
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; }
好了,到这里咱们讲解完了equals,仍是比较简单,那么重写equals时为什么必定要重写hashCode呢?主要缘由在于:这是通用约定,若是是基于散列的集合比较HashMap或者HashSet等,存储对象地址须要经过散列函数计算hashCode,如若不这样作将会出现意想不到的问题。那么意想不到的问题是什么呢?
下面咱们用一个例子来说解为什么重写equals时必定要重写hashCode。
public class Person { int age; String name; public Person(int age, String name) { this.age = age; this.name = name; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj instanceof Person) { Person p = (Person) obj; return (this.age == p.age && this.name == p.name); } return false; } }
如上咱们给出一个Person对象,而后带有年龄和名称两个属性,重写时判断年龄和名称相等便可认为为同一人,下面咱们在控制台进行以下操做,而后咱们看看将会打印出什么结果呢。
Person p1 = new Person(12, "Jeffcky"); Person p2 = new Person(12, "Jeffcky"); Hashtable hashtable = new Hashtable(); hashtable.put(p1, "v1"); System.out.println(hashtable.get(p2));
不难理解,由于Hashtable对象存储地址是基于hashCode,可是上述咱们没有重写hashCode,因此咱们实例化对象p2时,即便重写了equals两个对象相等,结果获取p2的值确定是获取不到的,由于hashCode不等,接下来咱们重写hashCode
@Override public int hashCode() { return (31 * Integer.valueOf(this.age).hashCode() + name.hashCode()); }
咱们看到字符串对象重写了hashCode,由于字符串用的很频繁,同时咱们极有可能在散列集合中用到。下面咱们来看看字符串对象的hashCode实现方式。
上图标记出的就是计算字符串的hashCode核心即散列函数,从上看出经过字符串中每个字符的ASCII码来计算,同时咱们也可再拓展下看源码数值类型的hashCode就是其自己。上述计算方式最终咱们数学进行概括出计算方法为:
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
好比咱们计算字符串【AC】的hashCode,根据如上计算公式则是
65*31^(2-1) + 67*31^(2-2) = 2082
在《Effective Java》中提到之因此选择31的缘由是:它是一个奇素数,若是乘数是偶数,而且乘法溢出的话,信息就会丢失,由于2相乘等价于移位运算。使用素数的好处并不很明显,可是习惯使用素数来计算散列结果。我严重怀疑是否是翻译的人理解错了意思,对于书中给出选择素数的缘由没法让人折服,这里我来说解我我的的想法。
选择31的缘由是由于它是质数(素数),而不是由于它是奇数。当咱们插入一个元素到哈希表中时,哈希如何识别须要将元素存储在哪一个存储桶中(Bucket)呢?这是一个重要的问题,使得强制性要求哈希可以在恒定时间内告诉咱们将值存储在哪一个存储桶中,以便可以快速检索。咱们能想到的是傻瓜式操做方式即循环遍历比较,这种顺序搜索将直接致使哈希性能恶化,直接取决哈希表所包含值的数量。换句话说,这将具备线性性能成本(O(N)),随着键(N)的数量愈来愈大,性能可想而知。另外一个复杂之处是咱们要处理的值的实际类型。若咱们要处理字符串和其余复杂类型,检查或比较自己的数量将致使成本又将变得很高。基于以上叙述,因此咱们至少须要解决两个问题,其一是便于快速检索而非顺序检索,其二是解决复杂类型值的比较。解决此问题的简单方法是但愿出现一种将复杂值分解为易于使用的键或哈希的方法,实现此过程的最简单方法是生成惟一编号,该数字必须是惟一的,由于咱们要区分一个值和另外一个值。质数是惟一数字,它们的独特之处在于,因为使用了素数来构成素数,所以素数与任何其余数字的乘积具备的最大可能的惟一性(不像素数自己那样惟一),质数的此属性在哈希函数中使用可减小冲突次数(或碰撞)。例如使用4 * 8,则它比诸如3 * 5的质数乘积更有可能发生冲突,32能够经过1 * 32或2 * 16或4 * 8或2 ^ 5等计算获得,但3*5 只能以1 * 15或3 * 5获得15。
本文咱们详细讨论了hashCode和equals,以及分析了在散列函数中使用质数的缘由,这里还存在一节内容留到学习虚拟机时再补上,经过分析虚拟机源码了解hashCode具体实现,下一节咱们将进入学习分析HashMap源码,感谢您的阅读,咱们下节见。